commit d0a5e428996c2b485949a3d76746fa18da20d06e Author: Fabio Date: Thu Mar 5 17:07:30 2026 +0100 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..c5313cf --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +BASE_URL=https://prova.patachina.it +SERVER_PORT=4000 +EMAIL=fabio@gmail.com +PASSWORD=master66 +JWT_SECRET=123456789 +JWT_EXPIRES=1h +PATH_FULL=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3eaf945 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +thumbs/ +db.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..30fed4a --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Galleria con json-server e protetto con JWT S6J + +## Installazione + +clonare questa repo e installare tutte le dipendenze con `npm ci` + + +## Start/Stop servers + +| Description | Script | +| ------------------------- | -------------------- | +| Start server senza auth | `npm start-no-auth` | +| Start server con auth | `npm run start` | + +## Tools + +| Description | Script | +| ------------------------------ | ------------------- | +| Generate user hashed passwords | `npm run hash` | + + +[json-server api reference](https://github.com/typicode/json-server) + +## Come usarlo + +clonare e poi installare con + +``` +npm ci +``` + +nel file .env ci sono tutti i dati da modificare + +poi inserire in user.json user e password utilizzati per fare il login + +la password da inserire è criptata e viene generata con npm run hash + +il nome viene utilizzato come cartella da scansionare, si trova dentro photos + +es: +``` +name: Fabio + +public/photos +└── Fabio + └── original + └── 2017Irlanda19-29ago + ├── IMG_0092.JPG + ├── IMG_0099.JPG + ├── IMG_0100.JPG +``` +poi dentro Fabio genererà thumbs con tutti i thumbs + +- npm run start +- su IP:4000 ci sarà la galleria e andando su impostazioni si potrà fare lo scan di tutte le foto + +dopo aver fatto lo scan è possibile richiedere il json al server con tutte le informazioni anche senza autorizzazione + +basta farlo partire con npm run start-no-auth e le info si possono vedere con + +ip:4000/photos + +- npm start + + +--- + +Inspired in this [post](https://www.techiediaries.com/fake-api-jwt-json-server/) by [Techiediaries](https://www.techiediaries.com/) + diff --git a/api_v1/config.js b/api_v1/config.js new file mode 100644 index 0000000..9b81e57 --- /dev/null +++ b/api_v1/config.js @@ -0,0 +1,17 @@ +require('dotenv').config(); +const path = require('path'); + +module.exports = { + BASE_URL: process.env.BASE_URL, + EMAIL: process.env.EMAIL, + PASSWORD: process.env.PASSWORD, + SEND_PHOTOS: (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true', + WRITE_INDEX: (process.env.WRITE_INDEX || 'true').toLowerCase() === 'true', + WEB_ROOT: process.env.WEB_ROOT || 'public', + PATH_FULL: (process.env.PATH_FULL || 'false').toLowerCase() === 'true', + INDEX_PATH: process.env.INDEX_PATH || path.posix.join('photos', 'index.json'), + SUPPORTED_EXTS: new Set([ + '.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif', + '.mp4', '.mov', '.m4v' + ]) +}; diff --git a/api_v1/geo.js b/api_v1/geo.js new file mode 100644 index 0000000..387f081 --- /dev/null +++ b/api_v1/geo.js @@ -0,0 +1,96 @@ +const axios = require("axios"); + +// Funzione principale +async function loc(lng, lat) { + const primary = await place(lng, lat); // Geoapify + const fallback = await placePhoton(lng, lat); // Photon + + // Se Geoapify fallisce → usa Photon + if (!primary) return fallback; + + // Se Geoapify manca city → prendi da Photon + if (!primary.city && fallback?.city) { + primary.city = fallback.city; + } + + // Se Geoapify manca postcode → prendi da Photon + if (!primary.postcode && fallback?.postcode) { + primary.postcode = fallback.postcode; + } + + // Se Geoapify manca address → prendi da Photon + if (!primary.address && fallback?.address) { + primary.address = fallback.address; + } + + // Se Geoapify manca region → prendi da Photon + if (!primary.region && fallback?.region) { + primary.region = fallback.region; + } + + // Se Geoapify manca county_code → Photon NON lo fornisce + // quindi non possiamo riempirlo + + return primary; +} + +// Geoapify (sorgente principale) +async function place(lng, lat) { + const apiKey = "6dc7fb95a3b246cfa0f3bcef5ce9ed9a"; + const url = `https://api.geoapify.com/v1/geocode/reverse?lat=${lat}&lon=${lng}&apiKey=${apiKey}`; + + try { + const r = await axios.get(url); + + if (r.status !== 200) return undefined; + if (!r.data.features || r.data.features.length === 0) return undefined; + + const k = r.data.features[0].properties; + + return { + continent: k?.timezone?.name?.split("/")?.[0] || undefined, + country: k?.country || undefined, + region: k?.state || undefined, + postcode: k?.postcode || undefined, + city: k?.city || undefined, + county_code: k?.county_code || undefined, + address: k?.address_line1 || undefined, + timezone: k?.timezone?.name || undefined, + time: k?.timezone?.offset_STD || undefined + }; + + } catch (err) { + return undefined; + } +} + +// Photon (fallback) +async function placePhoton(lng, lat) { + try { + const url = `https://photon.patachina.it/reverse?lon=${lng}&lat=${lat}`; + const r = await axios.get(url); + + if (!r.data || !r.data.features || r.data.features.length === 0) { + return undefined; + } + + const p = r.data.features[0].properties; + + return { + continent: undefined, // Photon non lo fornisce + country: p.country || undefined, + region: p.state || undefined, + postcode: p.postcode || undefined, + city: p.city || p.town || p.village || undefined, + county_code: undefined, // Photon non fornisce codici ISO + address: p.street ? `${p.street} ${p.housenumber || ""}`.trim() : undefined, + timezone: undefined, + time: undefined + }; + + } catch (err) { + return undefined; + } +} + +module.exports = loc; diff --git a/api_v1/initialDB.json b/api_v1/initialDB.json new file mode 100644 index 0000000..5a4f5e9 --- /dev/null +++ b/api_v1/initialDB.json @@ -0,0 +1 @@ +{"photos":[]} \ No newline at end of file diff --git a/api_v1/scanner/gps.js b/api_v1/scanner/gps.js new file mode 100644 index 0000000..b6f1bb0 --- /dev/null +++ b/api_v1/scanner/gps.js @@ -0,0 +1,69 @@ +const { exec } = require('child_process'); + +// ----------------------------------------------------------------------------- +// FOTO: GPS da ExifReader +// ----------------------------------------------------------------------------- +function extractGpsFromExif(tags) { + if (!tags?.gps) return null; + + const lat = tags.gps.Latitude; + const lng = tags.gps.Longitude; + const alt = tags.gps.Altitude; + + if (lat == null || lng == null) return null; + + return { + lat: Number(lat), + lng: Number(lng), + alt: alt != null ? Number(alt) : null + }; +} + +// ----------------------------------------------------------------------------- +// VIDEO: GPS via exiftool (VERSIONE ORIGINALE CHE FUNZIONA) +// ----------------------------------------------------------------------------- +function extractGpsWithExiftool(videoPath) { + //console.log(videoPath); + return new Promise((resolve) => { + const cmd = `exiftool -n -G1 -a -gps:all -quicktime:all -user:all "${videoPath}"`; + + exec(cmd, (err, stdout) => { + if (err || !stdout) return resolve(null); + + // 1) GPS Coordinates : + const userData = stdout.match(/GPS Coordinates\s*:\s*([0-9.\-]+)\s+([0-9.\-]+)/i); + if (userData) { + return resolve({ + lat: Number(userData[1]), + lng: Number(userData[2]), + alt: null + }); + } + + // 2) GPSLatitude / GPSLongitude + const lat1 = stdout.match(/GPSLatitude\s*:\s*([0-9.\-]+)/i); + const lng1 = stdout.match(/GPSLongitude\s*:\s*([0-9.\-]+)/i); + if (lat1 && lng1) { + return resolve({ + lat: Number(lat1[1]), + lng: Number(lng1[1]), + alt: null + }); + } + + // 3) GPSCoordinates : + const coords = stdout.match(/GPSCoordinates\s*:\s*([0-9.\-]+)\s+([0-9.\-]+)/i); + if (coords) { + return resolve({ + lat: Number(coords[1]), + lng: Number(coords[2]), + alt: null + }); + } + + resolve(null); + }); + }); +} + +module.exports = { extractGpsFromExif, extractGpsWithExiftool }; diff --git a/api_v1/scanner/indexStore.js b/api_v1/scanner/indexStore.js new file mode 100644 index 0000000..258b682 --- /dev/null +++ b/api_v1/scanner/indexStore.js @@ -0,0 +1,47 @@ +// scanner/indexStore.js +const path = require('path'); +const fsp = require('fs/promises'); +const { WEB_ROOT, INDEX_PATH } = require('../config'); + +// NOTE: siamo in .../api_v1/scanner → per arrivare alla root progetto servono due '..' +const absIndexPath = path.resolve(__dirname, '..', '..', WEB_ROOT, INDEX_PATH); +const absIndexTmp = absIndexPath + '.tmp'; + +const PRETTY = process.env.INDEX_PRETTY === 'true'; + +/** + * Carica l'indice. Formato canonico: mappa { [id]: meta }. + * Retro-compat: se il file è un array, convertilo in mappa. + */ +async function loadPreviousIndex() { + try { + const raw = await fsp.readFile(absIndexPath, 'utf8'); + const json = JSON.parse(raw); + + if (Array.isArray(json)) { + const map = {}; + for (const p of json) { + if (p && p.id) map[p.id] = p; + } + return map; + } + + return json && typeof json === 'object' ? json : {}; + } catch { + return {}; + } +} + +/** + * Salva l'indice (mappa) in modo atomico: scrive su .tmp e poi rename. + */ +async function saveIndex(indexMap) { + const dir = path.dirname(absIndexPath); + await fsp.mkdir(dir, { recursive: true }); + const data = PRETTY ? JSON.stringify(indexMap, null, 2) : JSON.stringify(indexMap); + + await fsp.writeFile(absIndexTmp, data, 'utf8'); + await fsp.rename(absIndexTmp, absIndexPath); +} + +module.exports = { loadPreviousIndex, saveIndex, absIndexPath }; \ No newline at end of file diff --git a/api_v1/scanner/postWithAuth.js b/api_v1/scanner/postWithAuth.js new file mode 100644 index 0000000..6d1f2f6 --- /dev/null +++ b/api_v1/scanner/postWithAuth.js @@ -0,0 +1,60 @@ +const axios = require('axios'); +const { BASE_URL, EMAIL, PASSWORD, SEND_PHOTOS } = require('../config'); + +let cachedToken = null; + +async function getToken(force = false) { + if (!SEND_PHOTOS) return null; + if (cachedToken && !force) return cachedToken; + + try { + const res = await axios.post(`${BASE_URL}/auth/login`, { + email: EMAIL, + password: PASSWORD + }); + + cachedToken = res.data.token; + return cachedToken; + + } catch (err) { + console.error('ERRORE LOGIN:', err.message); + return null; + } +} + +async function postWithAuth(url, payload) { + if (!SEND_PHOTOS) return; + + let token = await getToken(); + if (!token) throw new Error('Token assente'); + + try { + await axios.post(url, payload, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + timeout: 20000, + }); + + } catch (err) { + if (err.response && err.response.status === 401) { + token = await getToken(true); + if (!token) throw err; + + await axios.post(url, payload, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + timeout: 20000, + }); + + } else { + throw err; + } + } +} + +module.exports = postWithAuth; + diff --git a/api_v1/scanner/processFile.js b/api_v1/scanner/processFile.js new file mode 100644 index 0000000..d886844 --- /dev/null +++ b/api_v1/scanner/processFile.js @@ -0,0 +1,117 @@ +const path = require('path'); +const fsp = require('fs/promises'); +const ExifReader = require('exifreader'); +const sharp = require('sharp'); + +const { sha256, inferMimeFromExt, parseExifDateUtc } = require('./utils'); +const { extractGpsFromExif, extractGpsWithExiftool } = require('./gps'); +const { createVideoThumbnail, createThumbnails } = require('./thumbs'); +const { probeVideo } = require('./video'); +const loc = require('../geo.js'); +const { WEB_ROOT, PATH_FULL } = require('../config'); + +async function processFile(userName, cartella, fileRelPath, absPath, ext, st) { + const isVideo = ['.mp4', '.mov', '.m4v'].includes(ext); + + console.log( + `PROCESSING → user=${userName} | cartella=${cartella} | file=${fileRelPath}` + ); + + const thumbBase = path.join( + WEB_ROOT, + 'photos', + userName, + 'thumbs', + cartella, + path.dirname(fileRelPath) + ); + + await fsp.mkdir(thumbBase, { recursive: true }); + + const baseName = path.parse(fileRelPath).name; + + const absThumbMin = path.join(thumbBase, `${baseName}_min.jpg`); + const absThumbAvg = path.join(thumbBase, `${baseName}_avg.jpg`); + + if (isVideo) { + await createVideoThumbnail(absPath, absThumbMin, absThumbAvg); + } else { + await createThumbnails(absPath, absThumbMin, absThumbAvg); + } + + let tags = {}; + try { tags = await ExifReader.load(absPath, { expanded: true }); } catch {} + + const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null; + const takenAtIso = parseExifDateUtc(timeRaw); + + let gps = isVideo + ? await extractGpsWithExiftool(absPath) + : extractGpsFromExif(tags); + + let width = null, height = null, duration = null; + + if (isVideo) { + const info = await probeVideo(absPath); + const stream = info.streams?.find(s => s.width && s.height); + if (stream) { + width = stream.width; + height = stream.height; + } + duration = info.format?.duration || null; + } else { + try { + const meta = await sharp(absPath).metadata(); + width = meta.width || null; + height = meta.height || null; + } catch {} + } + + const mime_type = inferMimeFromExt(ext); + const id = sha256(`${userName}/${cartella}/${fileRelPath}`); + + const location = gps ? await loc(gps.lng, gps.lat) : null; + + // + // --- GESTIONE PATH FULL / RELATIVI --- + // + + // relativi (comportamento attuale) + const relPath = fileRelPath; + const relThub1 = fileRelPath.replace(/\.[^.]+$/, '_min.jpg'); + const relThub2 = fileRelPath.replace(/\.[^.]+$/, '_avg.jpg'); + + // completi (solo se PATH_FULL = true) + const fullPath = PATH_FULL + ? path.posix.join('/photos', userName, cartella, fileRelPath) + : relPath; + + const fullThub1 = PATH_FULL + ? path.posix.join('/photos', userName, 'thumbs', cartella, relThub1) + : relThub1; + + const fullThub2 = PATH_FULL + ? path.posix.join('/photos', userName, 'thumbs', cartella, relThub2) + : relThub2; + + return { + id, + user: userName, + cartella, + path: fullPath, + thub1: fullThub1, + thub2: fullThub2, + gps, + data: timeRaw, + taken_at: takenAtIso, + mime_type, + width, + height, + size_bytes: st.size, + mtimeMs: st.mtimeMs, + duration: isVideo ? duration : null, + location + }; +} + +module.exports = processFile; diff --git a/api_v1/scanner/scanCartella.js b/api_v1/scanner/scanCartella.js new file mode 100644 index 0000000..1cc03dd --- /dev/null +++ b/api_v1/scanner/scanCartella.js @@ -0,0 +1,58 @@ +// scanner/scanCartella.js +const path = require('path'); +const fsp = require('fs/promises'); +const processFile = require('./processFile'); +const { sha256 } = require('./utils'); +const { SUPPORTED_EXTS } = require('../config'); + +/** + * Scansiona ricorsivamente una cartella e ritorna SOLO i cambiamenti + * (nuovi/modificati) rispetto a previousIndex (mappa { id: meta }). + */ +async function scanCartella(userName, cartella, absCartella, previousIndex) { + const changes = []; + + async function walk(currentAbs, relPath = '') { + const entries = await fsp.readdir(currentAbs, { withFileTypes: true }); + for (const e of entries) { + const absPath = path.join(currentAbs, e.name); + if (e.isDirectory()) { + await walk(absPath, path.join(relPath, e.name)); + continue; + } + + const ext = path.extname(e.name).toLowerCase(); + if (!SUPPORTED_EXTS.has(ext)) continue; + + const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name; + const id = sha256(`${userName}/${cartella}/${fileRelPath}`); + + const st = await fsp.stat(absPath); + const prev = previousIndex[id]; + + const unchanged = + prev && + prev.size_bytes === st.size && + prev.mtimeMs === st.mtimeMs; + + if (unchanged) continue; // NOTE: skip "unchanged" + + const meta = await processFile( + userName, + cartella, + fileRelPath, + absPath, + ext, + st + ); + + meta.id = id; // id sempre presente + changes.push(meta); + } + } + + await walk(absCartella); + return changes; +} + +module.exports = scanCartella; \ No newline at end of file diff --git a/api_v1/scanner/scanPhoto.js b/api_v1/scanner/scanPhoto.js new file mode 100644 index 0000000..761e291 --- /dev/null +++ b/api_v1/scanner/scanPhoto.js @@ -0,0 +1,134 @@ +// scanner/scanPhoto.js +const path = require('path'); +const fsp = require('fs/promises'); +const { loadPreviousIndex, saveIndex } = require('./indexStore'); +const scanUserRoot = require('./scanUser'); +const postWithAuth = require('./postWithAuth'); +const { + WEB_ROOT, + SEND_PHOTOS, + BASE_URL, + WRITE_INDEX, +} = require('../config'); + +const COMMON = 'Common'; // Nome canonico e case-sensitive per la shared folder + +/** + * Restituisce la prima directory esistente tra le candidate + * @param {string[]} candidates + * @returns {Promise} + */ +async function firstExistingDir(candidates) { + for (const dir of candidates) { + try { + const st = await fsp.stat(dir); + if (st.isDirectory()) return dir; + } catch { /* ignore */ } + } + return null; +} + +/** + * Scansione foto con indice separato: + * - Carica indice precedente (mappa { id: meta }) + * - Produce SOLO i cambiamenti (nuovi/modificati) + * - Merge e salvataggio atomico dell'indice + * - POST verso /photos solo del delta (se abilitato) + * - Admin: include la cartella "Common" + * + * Nota path: usiamo un path ASSOLUTO per /public/photos + * partendo da .../api_v1/scanner -> '..', '..' per salire alla root del progetto. + */ +async function scanPhoto(dir, userName) { + try { + const previousIndexMap = await loadPreviousIndex(); + const nextIndexMap = { ...previousIndexMap }; + + // Path assoluto alla radice photos (es: /public/photos) + const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos'); + + let changes = []; + + if (userName === 'Admin') { + // 1) Scansiona TUTTI gli utenti tranne la/e cartella/e common + let entries = []; + try { + entries = await fsp.readdir(photosRoot, { withFileTypes: true }); + } catch (e) { + console.error(`[SCAN] photosRoot non accessibile: ${photosRoot}`, e.message); + entries = []; + } + + for (const e of entries) { + if (!e.isDirectory()) continue; + // salta qualunque "common" (qualunque casing) per non doppiare + if (e.name.toLowerCase() === COMMON.toLowerCase()) continue; + + const userDir = path.join(photosRoot, e.name, 'original'); + try { + const st = await fsp.stat(userDir); + if (!st.isDirectory()) continue; + } catch { + continue; + } + + const userChanges = await scanUserRoot(e.name, userDir, previousIndexMap); + for (const m of userChanges) nextIndexMap[m.id] = m; + changes.push(...userChanges); + } + + // 2) Scansiona la cartella COMMON (case strict), con fallback legacy (solo lettura) a 'common' + const commonPreferred = path.join(photosRoot, COMMON, 'original'); // .../photos/Common/original + const commonLegacy = path.join(photosRoot, 'common', 'original'); // .../photos/common/original (legacy) + + const commonDir = await firstExistingDir([commonPreferred, commonLegacy]); + if (commonDir) { + // Forziamo SEMPRE userName = 'Common' per ID/thumbnails coerenti in 'Common' + const commonChanges = await scanUserRoot(COMMON, commonDir, previousIndexMap); + for (const m of commonChanges) nextIndexMap[m.id] = m; + changes.push(...commonChanges); + console.log(`[SCAN] Common indicizzati da ${commonDir}: +${commonChanges.length}`); + } else { + console.log(`[SCAN] Nessuna cartella "${COMMON}" trovata sotto ${photosRoot} (atteso: ${commonPreferred})`); + } + + } else { + // Non-Admin (per completezza; in server.js /scan è Admin-only) + const userDir = path.join(photosRoot, userName, 'original'); + try { + const st = await fsp.stat(userDir); + if (st.isDirectory()) { + const userChanges = await scanUserRoot(userName, userDir, previousIndexMap); + for (const m of userChanges) nextIndexMap[m.id] = m; + changes.push(...userChanges); + } + } catch { + // utente senza dir 'original' -> nessuna modifica + } + } + + // Salva indice (mappa) in modo atomico + if (WRITE_INDEX) { + await saveIndex(nextIndexMap); + } + + // POST solo dei cambiamenti (delta) + if (SEND_PHOTOS && BASE_URL && changes.length) { + for (const p of changes) { + try { + await postWithAuth(`${BASE_URL}/photos`, p); + } catch (err) { + console.error('Errore invio:', err.message); + } + } + } + + console.log(`SCAN COMPLETATA per utente ${userName}: ${changes.length} file aggiornati`); + return changes; + } catch (e) { + console.error('Errore generale scanPhoto:', e); + throw e; + } +} + +module.exports = scanPhoto; \ No newline at end of file diff --git a/api_v1/scanner/scanUser.js b/api_v1/scanner/scanUser.js new file mode 100644 index 0000000..fcd2399 --- /dev/null +++ b/api_v1/scanner/scanUser.js @@ -0,0 +1,64 @@ +// scanner/scanUser.js +const path = require('path'); +const fsp = require('fs/promises'); +const scanCartella = require('./scanCartella'); +const processFile = require('./processFile'); +const { sha256 } = require('./utils'); +const { SUPPORTED_EXTS } = require('../config'); + +/** + * Scansiona la root dell'utente (p.es. ...//original) e: + * - indicizza i file direttamente al root (cartella virtuale "_root") + * - per ogni sottocartella, chiama scanCartella + */ +async function scanUserRoot(userName, userDir, previousIndex) { + const results = []; + const entries = await fsp.readdir(userDir, { withFileTypes: true }); + + // 1) File direttamente al root (virtual folder "_root") + for (const e of entries) { + if (e.isDirectory()) continue; + + const ext = path.extname(e.name).toLowerCase(); + if (!SUPPORTED_EXTS.has(ext)) continue; + + const absPath = path.join(userDir, e.name); + const st = await fsp.stat(absPath); + + const fileRelPath = e.name; + const cartella = '_root'; + const id = sha256(`${userName}/${cartella}/${fileRelPath}`); + + const prev = previousIndex[id]; + const unchanged = + prev && + prev.size_bytes === st.size && + prev.mtimeMs === st.mtimeMs; + + if (unchanged) continue; + + const meta = await processFile( + userName, + cartella, + fileRelPath, + absPath, + ext, + st + ); + meta.id = id; + results.push(meta); + } + + // 2) Sottocartelle (comportamento classico) + for (const e of entries) { + if (!e.isDirectory()) continue; + const cartella = e.name; + const absCartella = path.join(userDir, cartella); + const files = await scanCartella(userName, cartella, absCartella, previousIndex); + results.push(...files); + } + + return results; +} + +module.exports = scanUserRoot; \ No newline at end of file diff --git a/api_v1/scanner/thumbs.js b/api_v1/scanner/thumbs.js new file mode 100644 index 0000000..2275436 --- /dev/null +++ b/api_v1/scanner/thumbs.js @@ -0,0 +1,31 @@ +const sharp = require('sharp'); +const { exec } = require('child_process'); + +function createVideoThumbnail(videoPath, thumbMinPath, thumbAvgPath) { + return new Promise((resolve) => { + const cmd = ` + ffmpeg -y -i "${videoPath}" -ss 00:00:01.000 -vframes 1 "${thumbAvgPath}" && + ffmpeg -y -i "${thumbAvgPath}" -vf "scale=100:-1" "${thumbMinPath}" + `; + exec(cmd, () => resolve()); + }); +} + +async function createThumbnails(filePath, thumbMinPath, thumbAvgPath) { + try { + await sharp(filePath) + .resize({ width: 100, height: 100, fit: 'inside', withoutEnlargement: true }) + .withMetadata() + .toFile(thumbMinPath); + + await sharp(filePath) + .resize({ width: 400, withoutEnlargement: true }) + .withMetadata() + .toFile(thumbAvgPath); + + } catch (err) { + console.error('Errore creazione thumbnails:', err.message, filePath); + } +} + +module.exports = { createVideoThumbnail, createThumbnails }; diff --git a/api_v1/scanner/utils.js b/api_v1/scanner/utils.js new file mode 100644 index 0000000..6db52e9 --- /dev/null +++ b/api_v1/scanner/utils.js @@ -0,0 +1,41 @@ +const path = require('path'); +const crypto = require('crypto'); + +function toPosix(p) { + return p.split(path.sep).join('/'); +} + +function sha256(s) { + return crypto.createHash('sha256').update(s).digest('hex'); +} + +function inferMimeFromExt(ext) { + switch (ext.toLowerCase()) { + case '.jpg': + case '.jpeg': return 'image/jpeg'; + case '.png': return 'image/png'; + case '.webp': return 'image/webp'; + case '.heic': + case '.heif': return 'image/heic'; + case '.mp4': return 'video/mp4'; + case '.mov': return 'video/quicktime'; + case '.m4v': return 'video/x-m4v'; + default: return 'application/octet-stream'; + } +} + +function parseExifDateUtc(s) { + if (!s) return null; + const re = /^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/; + const m = re.exec(s); + if (!m) return null; + const dt = new Date(Date.UTC(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +m[6])); + return dt.toISOString(); +} + +module.exports = { + toPosix, + sha256, + inferMimeFromExt, + parseExifDateUtc +}; diff --git a/api_v1/scanner/video.js b/api_v1/scanner/video.js new file mode 100644 index 0000000..7d428fe --- /dev/null +++ b/api_v1/scanner/video.js @@ -0,0 +1,17 @@ +const { exec } = require('child_process'); + +function probeVideo(videoPath) { + return new Promise((resolve) => { + const cmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`; + exec(cmd, (err, stdout) => { + if (err) return resolve({}); + try { + resolve(JSON.parse(stdout)); + } catch { + resolve({}); + } + }); + }); +} + +module.exports = { probeVideo }; diff --git a/api_v1/server.js b/api_v1/server.js new file mode 100644 index 0000000..708b963 --- /dev/null +++ b/api_v1/server.js @@ -0,0 +1,306 @@ +// server.js +require('dotenv').config(); + +const fs = require('fs'); +const fsp = require('fs/promises'); +const path = require('path'); +const jsonServer = require('json-server'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcrypt'); + +const scanPhoto = require('./scanner/scanPhoto.js'); +const { WEB_ROOT, INDEX_PATH } = require('./config'); + +const SECRET_KEY = process.env.JWT_SECRET || '123456789'; +const EXPIRES_IN = process.env.JWT_EXPIRES || '1h'; +const PORT = process.env.SERVER_PORT || 4000; + +const server = jsonServer.create(); + +// ----------------------------------------------------- +// STATIC FILES +// ----------------------------------------------------- +server.use( + jsonServer.defaults({ + static: path.join(__dirname, '../public'), + }) +); + +// ----------------------------------------------------- +// CONFIG ENDPOINT (PUBBLICO) +// ----------------------------------------------------- +server.get('/config', (req, res) => { + res.json({ + baseUrl: process.env.BASE_URL, + pathFull: process.env.PATH_FULL, + }); +}); + +// ----------------------------------------------------- +// ROUTER DB +// ----------------------------------------------------- +let router; +if (fs.existsSync('./api_v1/db.json')) { + router = jsonServer.router('./api_v1/db.json'); +} else { + const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8'); + fs.writeFileSync('api_v1/db.json', initialData); + router = jsonServer.router('./api_v1/db.json'); +} + +// ----------------------------------------------------- +// USERS DB +// ----------------------------------------------------- +const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'utf-8')); +server.use(jsonServer.bodyParser); + +// ----------------------------------------------------- +// JWT HELPERS +// ----------------------------------------------------- +function createToken(payload) { + return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN }); +} + +const denylist = new Map(); + +function addToDenylist(token) { + try { + const decoded = jwt.decode(token); + const exp = decoded?.exp || Math.floor(Date.now() / 1000) + 60; + denylist.set(token, exp); + } catch { + denylist.set(token, Math.floor(Date.now() / 1000) + 60); + } +} + +function isRevoked(token) { + const exp = denylist.get(token); + if (!exp) return false; + if (exp * 1000 < Date.now()) { + denylist.delete(token); + return false; + } + return true; +} + +function verifyToken(token) { + return jwt.verify(token, SECRET_KEY); +} + +function isAuthenticated({ email, password }) { + return ( + userdb.users.findIndex( + (user) => user.email === email && bcrypt.compareSync(password, user.password) + ) !== -1 + ); +} + +// ----------------------------------------------------- +// RESET DB (utility interna usata da /initDB) +// ----------------------------------------------------- +function resetDB() { + const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8'); + fs.writeFileSync('api_v1/db.json', initialData); + router.db.setState(JSON.parse(initialData)); + console.log('DB resettato'); +} + +// ----------------------------------------------------- +// HOME +// ----------------------------------------------------- +server.get('/', (req, res) => { + res.sendFile(path.resolve('public/index.html')); +}); + +// ----------------------------------------------------- +// LOGIN (PUBBLICO) +// ----------------------------------------------------- +server.post('/auth/login', (req, res) => { + const { email, password } = req.body; + const user = userdb.users.find((u) => u.email === email); + + if (!user || !bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ status: 401, message: 'Incorrect email or password' }); + } + + const token = createToken({ + id: user.id, + email: user.email, + name: user.name, + }); + + res.status(200).json({ + token, + name: user.name, + }); +}); + +// ----------------------------------------------------- +// LOGOUT +// ----------------------------------------------------- +server.post('/auth/logout', (req, res) => { + const auth = req.headers.authorization || ''; + const [scheme, token] = auth.split(' '); + if (scheme === 'Bearer' && token) { + addToDenylist(token); + } + return res.status(204).end(); +}); + +// ----------------------------------------------------- +// JWT MIDDLEWARE (tutte le rotte tranne /auth/*) +// ----------------------------------------------------- +server.use(/^(?!\/auth).*$/, (req, res, next) => { + const auth = req.headers.authorization || ''; + const [scheme, token] = auth.split(' '); + + if (scheme !== 'Bearer' || !token) { + return res.status(401).json({ status: 401, message: 'Bad authorization header' }); + } + if (isRevoked(token)) { + return res.status(401).json({ status: 401, message: 'Token revoked' }); + } + + try { + const decoded = verifyToken(token); + req.user = decoded; + next(); + } catch (err) { + return res + .status(401) + .json({ status: 401, message: 'Error: access_token is not valid' }); + } +}); + +// ----------------------------------------------------- +// FILTRO AUTOMATICO PER USER (GET) +// - Non-Admin: forzo user=[, 'Common'] così vedono anche la Common +// - Admin: vede tutto senza forzature +// ----------------------------------------------------- +server.use((req, res, next) => { + if (req.method === 'GET' && req.user && req.user.name !== 'Admin') { + const u = req.user.name; + const q = req.query.user; + const base = q ? (Array.isArray(q) ? q : [q]) : []; + req.query.user = Array.from(new Set([...base, u, 'Common'])); + } + next(); +}); + +// ----------------------------------------------------- +// SCAN FOTO +// - Admin: scansiona tutti gli utenti + Common +// - Non-Admin: scansiona solo la propria area (NO Common) +// ----------------------------------------------------- +server.get('/scan', async (req, res) => { + try { + if (req.user && req.user.name === 'Admin') { + await scanPhoto(undefined, 'Admin'); + return res.send({ + status: 'Scansione completata', + user: 'Admin', + scope: 'tutti gli utenti + Common', + }); + } + + // Non-Admin → solo la sua area (niente Common) + await scanPhoto(undefined, req.user.name); + res.send({ + status: 'Scansione completata', + user: req.user.name, + scope: 'utente corrente', + }); + } catch (err) { + console.error('Errore scan:', err); + res.status(500).json({ error: 'Errore durante lo scan', details: err.message }); + } +}); + +// ----------------------------------------------------- +// FILE STATICI +// ----------------------------------------------------- +server.get('/files', (req, res) => { + const requested = req.query.file || ''; + const publicDir = path.resolve(path.join(__dirname, '../public')); + const resolved = path.resolve(publicDir, requested); + + if (!resolved.startsWith(publicDir)) { + return res.status(400).json({ error: 'Invalid path' }); + } + if (!fs.existsSync(resolved) || fs.statSync(resolved).isDirectory()) { + return res.status(404).json({ error: 'Not found' }); + } + res.sendFile(resolved); +}); + +// ----------------------------------------------------- +// RESET DB MANUALE + rimozione index.json +// ----------------------------------------------------- +server.get('/initDB', async (req, res) => { + try { + resetDB(); + + // /public/photos/index.json (coerente con indexStore.js) + const absIndexPath = path.resolve(__dirname, '..', WEB_ROOT, INDEX_PATH); + + try { + await fsp.unlink(absIndexPath); + console.log('initDB: index.json rimosso ->', absIndexPath); + } catch (err) { + if (err && err.code === 'ENOENT') { + console.log('initDB: index.json non trovato, niente da cancellare:', absIndexPath); + } else { + console.error('initDB: errore cancellando index.json:', err); + } + } + + res.json({ status: 'DB resettato', indexRemoved: true, indexPath: absIndexPath }); + } catch (err) { + console.error('initDB: errore generale:', err); + res.status(500).json({ status: 'errore', message: err.message }); + } +}); + +// ----------------------------------------------------- +// UPSERT anti-duplicato per /photos (prima del router) +// Se id esiste -> aggiorna; altrimenti crea +// ----------------------------------------------------- +server.post('/photos', (req, res, next) => { + try { + const id = req.body && req.body.id; + if (!id) return next(); + + const db = router.db; // lowdb instance + const col = db.get('photos'); + const existing = col.find({ id }).value(); + + if (existing) { + col.find({ id }).assign(req.body).write(); + return res.status(200).json(req.body); + } + return next(); // non esiste: crea con il router + } catch (e) { + console.error('UPSERT /photos error:', e); + return res.status(500).json({ error: 'upsert failed' }); + } +}); + +// ----------------------------------------------------- +// ROUTER JSON-SERVER +// ----------------------------------------------------- +server.use(router); + +// ----------------------------------------------------- +// START SERVER +// ----------------------------------------------------- +server.listen(PORT, () => { + console.log(`Auth API server running on port ${PORT} ...`); +}); + +// Pulizia denylist +setInterval(() => { + const nowSec = Math.floor(Date.now() / 1000); + for (const [tok, exp] of denylist.entries()) { + if (exp < nowSec) denylist.delete(tok); + } +}, 60 * 1000); \ No newline at end of file diff --git a/api_v1/tools.js b/api_v1/tools.js new file mode 100644 index 0000000..13fca1d --- /dev/null +++ b/api_v1/tools.js @@ -0,0 +1,51 @@ +/** + * Required libraries + */ +const bcrypt = require('bcrypt') +const readLine = require('readline') +const async = require('async') + +// Password hash method +const hashPassword = plain => bcrypt.hashSync(plain, 8) + +// Ask user password method +function askPassword(question) { + return new Promise((resolve, reject) => { + const rl = readLine.createInterface({ + input: process.stdin, + output: process.stdout + }) + + rl.question(question, answer => { + rl.close() + resolve(answer) + }) + }) +} + +// Generate hash password method +async function generateHash() { + try { + console.log('**********************************') + console.log('** Password hash script **') + console.log('**********************************') + + const passwordAnswer = await askPassword( + 'Please give me a password to hash: ' + ) + + if (passwordAnswer != '') { + const hashedPassword = hashPassword(passwordAnswer) + const compare = bcrypt.compareSync(passwordAnswer, hashedPassword) + await console.log('Hashed password:', hashedPassword) + await console.log('Valdiation:', compare) + } else { + console.log('You need write something. Script aborted!') + } + } catch (err) { + console.log(err) + return process.exit(1) + } +} + +generateHash() diff --git a/api_v1/users.json b/api_v1/users.json new file mode 100644 index 0000000..db83a6d --- /dev/null +++ b/api_v1/users.json @@ -0,0 +1,22 @@ +{ + "users": [ + { + "id": 1, + "name": "Admin", + "email": "admin@gmail.com", + "password": "$2b$08$g0UWN6RnN7e.8rX3fuXSSOSJDTvucu./0FAU.yXp0wx4SJXyeaU3." + }, + { + "id": 2, + "name": "Fabio", + "email": "fabio@gmail.com", + "password": "$2b$08$g0UWN6RnN7e.8rX3fuXSSOSJDTvucu./0FAU.yXp0wx4SJXyeaU3." + }, + { + "id": 3, + "name": "Jessica", + "email": "jessie@libero.it", + "password": "$2b$08$g0UWN6RnN7e.8rX3fuXSSOSJDTvucu./0FAU.yXp0wx4SJXyeaU3." + } + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..50c62bf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2570 @@ +{ + "name": "ssj", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ssj", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "async": "^3.2.6", + "axios": "^1.13.5", + "bcrypt": "^6.0.0", + "body-parser": "^2.2.2", + "dotenv": "^17.3.1", + "exifreader": "^4.36.2", + "json-server": "^0.17.4", + "jsonwebtoken": "^9.0.3", + "path": "^0.12.7", + "sharp": "^0.34.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz", + "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "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.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "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/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/connect-pause": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz", + "integrity": "sha512-a1gSWQBQD73krFXdUEYJom2RTFrWUL3YvXDCRkyv//GVXc79cdW9MngtRuN9ih4FDKBtfJAJId+BbDuX+1rh2w==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/errorhandler": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.2.tgz", + "integrity": "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/exifreader": { + "version": "4.36.2", + "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.36.2.tgz", + "integrity": "sha512-Rpboqge86aBhRVJeW70BZHIkFNCi6rVlidzKuDyRxsreS/SbT983wFk/88+ddJu7zSrOae5fuiyXO7X91qq88Q==", + "hasInstallScript": true, + "license": "MPL-2.0", + "optionalDependencies": { + "@xmldom/xmldom": "^0.9.4" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-urlrewrite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-1.4.0.tgz", + "integrity": "sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA==", + "license": "MIT", + "dependencies": { + "debug": "*", + "path-to-regexp": "^1.0.3" + } + }, + "node_modules/express-urlrewrite/node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "license": "MIT" + }, + "node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==", + "license": "MIT", + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/json-server": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/json-server/-/json-server-0.17.4.tgz", + "integrity": "sha512-bGBb0WtFuAKbgI7JV3A864irWnMZSvBYRJbohaOuatHwKSRFUfqtQlrYMrB6WbalXy/cJabyjlb7JkHli6dYjQ==", + "license": "MIT", + "dependencies": { + "body-parser": "^1.19.0", + "chalk": "^4.1.2", + "compression": "^1.7.4", + "connect-pause": "^0.1.1", + "cors": "^2.8.5", + "errorhandler": "^1.5.1", + "express": "^4.17.1", + "express-urlrewrite": "^1.4.0", + "json-parse-helpfulerror": "^1.0.3", + "lodash": "^4.17.21", + "lodash-id": "^0.14.1", + "lowdb": "^1.0.0", + "method-override": "^3.0.0", + "morgan": "^1.10.0", + "nanoid": "^3.1.23", + "please-upgrade-node": "^3.2.0", + "pluralize": "^8.0.0", + "server-destroy": "^1.0.1", + "yargs": "^17.0.1" + }, + "bin": { + "json-server": "lib/cli/bin.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/json-server/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/json-server/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/json-server/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/json-server/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/json-server/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/json-server/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-server/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/json-server/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-id": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/lodash-id/-/lodash-id-0.14.1.tgz", + "integrity": "sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "license": "MIT", + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/method-override/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "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/mime-types/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/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.3" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b46941b --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "gallery-jwt-json-server", + "version": "0.0.1", + "description": "Gallery and JWT Protected REST API with json-server", + "main": "index.js", + "scripts": { + "start-no-auth": "json-server --watch ./api_v1/db.json -s ./public --host 0.0.0.0 --port 4000", + "start": "node ./api_v1/server.js -s ./public", + "hash": "node ./api_v1/tools.js" + }, + "keywords": [ + "api" + ], + "author": "Fabio", + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "axios": "^1.13.5", + "bcrypt": "^6.0.0", + "body-parser": "^2.2.2", + "dotenv": "^17.3.1", + "exifreader": "^4.36.2", + "json-server": "^0.17.4", + "jsonwebtoken": "^9.0.3", + "path": "^0.12.7", + "sharp": "^0.34.5" + } +} diff --git a/public/admin.html b/public/admin.html new file mode 100644 index 0000000..3511b5a --- /dev/null +++ b/public/admin.html @@ -0,0 +1,70 @@ + + + + + Photo Manager + + + +
+

Gestione Foto

+ + + + + + +

+
+ + + + + + + diff --git a/public/css/base.css b/public/css/base.css new file mode 100644 index 0000000..1111f49 --- /dev/null +++ b/public/css/base.css @@ -0,0 +1,27 @@ +:root { --header-h: 60px; } + +/* Safe-area iOS */ +@supports (top: env(safe-area-inset-top)) { + :root { --safe-top: env(safe-area-inset-top); } +} +@supports not (top: env(safe-area-inset-top)) { + :root { --safe-top: 0px; } +} + +body { + font-family: sans-serif; + margin: 0; + padding: 0; + background: #fafafa; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 4px; +} diff --git a/public/css/bottomSheet.css b/public/css/bottomSheet.css new file mode 100644 index 0000000..bcf21cf --- /dev/null +++ b/public/css/bottomSheet.css @@ -0,0 +1,200 @@ +/* ========================================= + Variabili globali + ========================================= */ +:root { + --header-height: 60px; /* cambia se il tuo header è più alto/basso */ +} + +/* ========================================= + MAPPA GLOBALE (contenitore sotto l’header) + ========================================= */ +.global-map { + position: fixed; + top: var(--header-height); + left: 0; + right: 0; + bottom: 0; + + width: 100%; + display: none; /* visibile solo con .open */ + z-index: 10; /* sotto a bottom-sheet (9999) e modal (10000) */ + background: #000; /* evita flash bianco durante init */ +} + +.global-map.open { + display: block; +} + +/* Leaflet riempie il contenitore */ +.global-map .leaflet-container { + width: 100%; + height: 100%; +} + +/* Marker immagine (miniatura) */ +.leaflet-marker-icon.photo-marker { + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.25); + border: 2px solid rgba(255,255,255,0.9); + background: #fff; +} + +/* Nascondi la gallery quando la mappa è aperta */ +.gallery.hidden { + display: none !important; +} + +/* ========================================= + BOTTOM SHEET — struttura base comune + ========================================= */ +.bottom-sheet { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + + background: rgba(255,255,255,0.95); + backdrop-filter: blur(6px); + border-top: 1px solid #ddd; + box-shadow: 0 -2px 10px rgba(0,0,0,0.15); + + display: none; /* diventa flex con .open */ + flex-direction: column; + z-index: 9999; /* molto alto: il modal starà sopra (10000) */ +} + +.bottom-sheet.open { + display: flex; +} + +/* Maniglia superiore */ +.sheet-header { + height: 16px; + display: flex; + justify-content: center; + align-items: center; +} + +.sheet-header::before { + content: ""; + width: 40px; + height: 4px; + background: #bbb; + border-radius: 4px; +} + +/* ========================================= + BOTTOM SHEET FOTO (strip bassa come nel vecchio) + ========================================= */ +.photo-strip { + height: 140px; /* altezza originale della strip */ + overflow-y: hidden; /* niente scroll verticale */ + overflow-x: auto; /* scroll orizzontale per le foto */ +} + +/* Contenitore elementi della strip — compatibile con id e class */ +#sheetGallery, +.sheet-gallery { + display: flex; + flex-direction: row; + overflow-x: auto; + padding: 10px; + gap: 10px; + + -webkit-overflow-scrolling: touch; + scroll-snap-type: x proximity; +} + +/* Singolo elemento della strip */ +.sheet-item { + width: 90px; + height: 90px; + border-radius: 10px; + overflow: hidden; + flex-shrink: 0; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + background: #eee; + scroll-snap-align: start; +} + +/* Miniatura della foto nella strip */ +.sheet-thumb, +.sheet-item img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + border-radius: 8px; /* alias; la .sheet-item ha già 10px */ +} + +/* ========================================= + BOTTOM SHEET OPZIONI (⋮) — menu grande + ========================================= */ +.options-sheet { + height: auto; + max-height: 80vh; + overflow-y: auto; +} + +.sheet-content { + padding: 20px; +} + +.sheet-btn { + width: 100%; + padding: 12px; + margin-bottom: 8px; + text-align: left; + background: #f5f5f5; + border: none; + border-radius: 8px; + font-size: 15px; + cursor: pointer; +} + +.sheet-btn:hover { + background: #e8e8e8; +} + +#optionsSheet h3 { + margin-top: 20px; + margin-bottom: 10px; + font-size: 16px; + color: #444; +} + +/* ========================================= + OVERLAY per chiusura sheet/option + ========================================= */ +.sheet-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.0); /* invisibile ma cliccabile */ + display: none; + z-index: 80; /* appena sotto il bottom sheet */ +} + +.sheet-overlay.open { + display: block; +} + +/* ========================================= + MODAL sopra allo sheet + ========================================= */ +.modal.open { + z-index: 10000 !important; /* sopra al bottom sheet (9999) */ +} + +/* ========================================= + Piccoli affinamenti facoltativi + ========================================= */ +/* scrollbar sottile solo per la strip (opzionale) */ +#sheetGallery::-webkit-scrollbar, +.sheet-gallery::-webkit-scrollbar { + height: 8px; +} +#sheetGallery::-webkit-scrollbar-thumb, +.sheet-gallery::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.25); + border-radius: 4px; +} \ No newline at end of file diff --git a/public/css/gallery.css b/public/css/gallery.css new file mode 100644 index 0000000..c834fe4 --- /dev/null +++ b/public/css/gallery.css @@ -0,0 +1,46 @@ +.gallery { + display: block; + padding: 6px; /* più stretto */ +} + +.gallery-section-title { + font-size: 18px; + font-weight: 600; + margin: 18px 6px 6px; /* più compatto */ + color: #444; +} + +.gallery-section { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); /* leggermente più piccole */ + gap: 4px; /* SPACING RIDOTTO */ + padding: 0 4px; +} + +.thumb { + width: 100%; + aspect-ratio: 1 / 1; + border-radius: 6px; /* più compatto */ + overflow: hidden; + background: white; + box-shadow: 0 1px 3px rgba(0,0,0,0.12); /* più leggero */ + position: relative; + cursor: pointer; +} + +.thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.play-icon { + position: absolute; + bottom: 4px; + right: 4px; + background: rgba(0,0,0,0.55); + color: white; + padding: 2px 4px; + border-radius: 4px; + font-size: 11px; +} diff --git a/public/css/header.css b/public/css/header.css new file mode 100644 index 0000000..49d786a --- /dev/null +++ b/public/css/header.css @@ -0,0 +1,85 @@ +/* =============================== + Header compatto + =============================== */ +header { + padding: 4px 10px; /* era 10px 15px */ + background: #333; + color: white; + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; + top: 0; + z-index: 100; +} + +/* Titolo più piccolo e senza margini extra */ +header h1 { + font-size: 18px; /* ridotto */ + line-height: 1.1; + margin: 0; +} + +/* Contenitore bottoni in alto a destra */ +.top-buttons { + display: flex; + gap: 6px; /* era 10px */ +} + +/* Bottoni icona più compatti */ +.icon-btn { + background: none; + border: none; + font-size: 18px; /* era 22px */ + padding: 3px 6px; /* era 6px 10px */ + cursor: pointer; + border-radius: 6px; + color: white; + line-height: 1; + min-height: 32px; /* tap target minimo desktop */ + min-width: 32px; +} + +.icon-btn:hover { + background: rgba(255,255,255,0.15); +} + +/* Logout rotondo: riduciamo la “bolla” */ +.icon-btn.logout-btn { + --size: 28px; /* era 36px */ + width: var(--size); + height: var(--size); + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* PNG del logout in scala con l’header */ +.logout-icon { + width: 18px; /* era 22px */ + height: 18px; + display: block; + filter: brightness(0) invert(1); + image-rendering: -webkit-optimize-contrast; +} + +/* =============================== + Visibilità Logout robusta + =============================== */ + +/* Base: nascosto (prima del login o se non autenticato) */ +#logoutBtn { + display: none; +} + +/* Quando autenticato, mostra il bottone coerente con gli altri icon-btn */ +body.authenticated #logoutBtn { + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* Se esistono regole più forti altrove che lo nascondono, + puoi temporaneamente forzare: + body.authenticated #logoutBtn { display: inline-flex !important; } */ \ No newline at end of file diff --git a/public/css/infoPanel.css b/public/css/infoPanel.css new file mode 100644 index 0000000..3edea8e --- /dev/null +++ b/public/css/infoPanel.css @@ -0,0 +1,88 @@ +/* =============================== + Variabili (scala tipografica pannello info) + Modifica qui per regolare tutto il pannello + =============================== */ +:root { + --info-font: 14px; /* base testo pannello (prima ~16px) */ + --info-line: 1.4; /* interlinea per migliorare leggibilità */ + + --info-heading: 15px; /* dimensione titoli h3 nel pannello */ + --info-h3-mt: 6px; /* margin-top h3 */ + --info-h3-mb: 10px; /* margin-bottom h3 */ + + --info-row-gap: 8px; /* spazio verticale tra righe (era 10px) */ + --info-label-w: 100px; /* larghezza colonna etichette (era 110px) */ + + --info-map-h: 220px; /* altezza mappa (era 250px) */ + --info-map-mt: 15px; /* spazio sopra la mappa */ + + --info-spacer-h: 16px; /* altezza degli spacer */ +} + +/* =============================== + PANNELLO INFO + =============================== */ +.info-panel { + position: fixed; + top: 0; + right: 0; + width: 320px; + height: 100%; + background: #fff; + padding: 16px; + box-shadow: -2px 0 6px rgba(0,0,0,0.25); + overflow-y: auto; + z-index: 10000; + transform: translateX(100%); + transition: transform 0.3s ease; + + /* Scala tipografica via variabili */ + font-size: var(--info-font); + line-height: var(--info-line); +} + +.info-panel.open { + transform: translateX(0); +} + +/* Titoli un filo più compatti */ +.info-panel h3 { + font-size: var(--info-heading); + margin: var(--info-h3-mt) 0 var(--info-h3-mb); +} + +/* Righe e label */ +.info-row { + margin-bottom: var(--info-row-gap); +} + +.info-row b { + display: inline-block; + width: var(--info-label-w); +} + +/* Mappa nel pannello */ +.info-map { + width: 100%; + height: var(--info-map-h); + margin-top: var(--info-map-mt); + border-radius: 6px; + overflow: hidden; + border: 1px solid #ccc; +} + +/* Spacer verticali */ +.info-spacer { + height: var(--info-spacer-h); +} + +/* =============================== + (Opzionale) Mobile: un filo più grande < 480px + Decommenta se vuoi mantenere leggibilità maggiore su schermi piccoli + =============================== */ +/* +@media (max-width: 480px) { + .info-panel { font-size: 15px; } + .info-panel h3 { font-size: 16px; } +} +*/ \ No newline at end of file diff --git a/public/css/login.css b/public/css/login.css new file mode 100644 index 0000000..91f5dc8 --- /dev/null +++ b/public/css/login.css @@ -0,0 +1,27 @@ + +.login-modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.7); + display: none; + align-items: center; + justify-content: center; + z-index: 20000; +} + +.login-box { + background: white; + padding: 20px; + border-radius: 12px; + width: 280px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.login-error { + color: red; + font-size: 14px; + min-height: 18px; +} + diff --git a/public/css/map.css b/public/css/map.css new file mode 100644 index 0000000..85a50dc --- /dev/null +++ b/public/css/map.css @@ -0,0 +1,117 @@ +/* =============================== + MAPPA GLOBALE + =============================== */ + +/* La mappa occupa tutto lo schermo SOTTO l’header */ +.global-map { + position: fixed; + left: 0; + right: 0; + top: calc(var(--header-h, 60px) + var(--safe-top, 0px)); /* niente hard-code */ + bottom: 0; + z-index: 50; + + display: none; /* chiusa di default */ +} + +/* Quando è aperta, visibile */ +.global-map.open { + display: block; +} + +/* La Leaflet container deve riempire il contenitore */ +.global-map, +.global-map .leaflet-container { + width: 100%; + height: 100%; +} + +/* Nasconde la gallery quando la mappa è aperta */ +.gallery.hidden { + display: none; +} + +/* =============================== + MARKER FOTO + =============================== */ + +.photo-marker { + width: 48px; + height: 48px; + border-radius: 10px; + overflow: hidden; + position: relative; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + background: #fff; +} + +.photo-marker img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* =============================== + CLUSTER + =============================== */ + +.photo-cluster { + width: 56px; + height: 56px; + position: relative; + border-radius: 12px; + overflow: visible; +} + +.cluster-back { + position: absolute; + top: 6px; + left: 6px; + width: 48px; + height: 48px; + border-radius: 10px; + object-fit: cover; + opacity: 0.5; + filter: blur(1px); + transform: scale(0.95); +} + +.cluster-front { + position: absolute; + top: 0; + left: 0; + width: 48px; + height: 48px; + border-radius: 10px; + object-fit: cover; + box-shadow: 0 2px 6px rgba(0,0,0,0.35); +} + + + +/* =============================== + MARKER CLUSTER + =============================== */ + + +.marker-cluster-wrapper { background: transparent; border: 0; } +.gp-cluster { + position: relative; + border-radius: 50%; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0,0,0,0.35); + border: 3px solid rgba(255,255,255,0.85); + transition: width .12s, height .12s, font-size .12s; +} +.gp-cluster .cluster-collage { position:absolute; inset:0; display:grid; grid-template-columns: repeat(2,1fr); grid-template-rows: repeat(2,1fr); } +.gp-cluster .cluster-collage div img { width:100%; height:100%; object-fit:cover; display:block; } +.gp-cluster .gp-count { + position:absolute; right:6px; bottom:6px; + background: rgba(0,0,0,0.55); padding:4px 7px; border-radius:12px; + color:#fff; font-weight:700; font-size:12px; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); +} +.gp-cluster.cluster-sm .gp-count { font-size:11px; } +.gp-cluster.cluster-md .gp-count { font-size:13px; } +.gp-cluster.cluster-lg .gp-count { font-size:15px; } +.gp-cluster.cluster-xl .gp-count { font-size:17px; } diff --git a/public/css/modal.css b/public/css/modal.css new file mode 100644 index 0000000..f3da73e --- /dev/null +++ b/public/css/modal.css @@ -0,0 +1,299 @@ +/* =============================== + MODAL OVERLAY + =============================== */ + +.modal { + position: fixed; + inset: 0; /* top:0 right:0 bottom:0 left:0 */ + background: rgba(0,0,0,0.8); + display: none; /* chiuso di default */ + align-items: center; + justify-content: center; + z-index: 9999; /* sopra a qualunque overlay/sheet */ + overflow: hidden; /* evita scroll sullo sfondo */ + /* Animazione di fade */ + opacity: 0; + transition: opacity 160ms ease-out; +} + +.modal.open { + display: flex; + opacity: 1; +} + +/* effetto vetro opzionale dove supportato */ +@supports (backdrop-filter: blur(4px)) { + .modal { + backdrop-filter: blur(4px); + } +} + +/* =============================== + CONTENITORE CONTENUTI + =============================== */ + +.modal-content { + width: 90vw; + height: 90vh; + max-width: 1200px; + max-height: 90vh; + position: relative; + display: flex; + justify-content: center; + align-items: center; + + /* Animazione di scale-in */ + transform: scale(0.98); + transition: transform 160ms ease-out; +} + +.modal.open .modal-content { + transform: scale(1); +} + +/* Ridimensionamento su mobile */ +@media (max-width: 768px) { + .modal-content { + width: 100vw; + height: 100vh; + } +} + +/* Contenitore del media */ +#modalMediaContainer { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + + /* Evita che clic sul media “passino” al layer sotto */ + position: relative; + z-index: 1; +} + +/* Immagini e video si adattano all’area */ +#modalMediaContainer img, +#modalMediaContainer video { + width: 100%; + height: 100%; + object-fit: contain; + display: block; + background: #000; /* evita flash bianco */ + position: relative; /* crea contesto */ + z-index: 1; /* sotto ai pulsanti */ +} + +/* =============================== + PULSANTE CHIUSURA (X) + =============================== */ + +/* FISSO sopra al video, con safe-area per iPhone */ +.modal-close { + position: fixed; /* <-- chiave: resta sopra al video anche con stacking strani */ + top: calc(8px + env(safe-area-inset-top)); + right: calc(12px + env(safe-area-inset-right)); + z-index: 10001; /* il modal è 9999 */ + + background: rgba(0,0,0,0.35); + color: #fff; + border-radius: 22px; + min-width: 44px; /* target minimo consigliato */ + height: 44px; + padding: 0 10px; + + display: inline-flex; + align-items: center; + justify-content: center; + + cursor: pointer; + font-weight: 700; + border: 1px solid rgba(255,255,255,0.25); + box-shadow: 0 2px 8px rgba(0,0,0,0.4); + user-select: none; + line-height: 1; +} + +/* area di hit più ampia senza cambiare il look */ +.modal-close::after { + content: ""; + position: absolute; + inset: -8px; /* allarga di 8px tutt’intorno */ +} + +.modal-close:hover { + background: rgba(0,0,0,0.5); +} + +.modal-close:active { + transform: translateY(1px); +} + +.modal-close:focus-visible { + outline: 2px solid #4c9ffe; + outline-offset: 2px; + border-radius: 8px; +} + +/* =============================== + PULSANTE INFO (ℹ️) + =============================== */ + +.modal-info-btn { + position: absolute; + bottom: 12px; + right: 16px; + + background: #fff; + border-radius: 50%; + width: 40px; + height: 40px; + + display: inline-flex; + align-items: center; + justify-content: center; + + cursor: pointer; + border: 1px solid #d0d0d0; + font-size: 20px; + z-index: 10000; /* sopra al media, sotto alla X */ + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + + /* 🔒 Disattiva selezione e popup dizionario */ + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; +} + +.modal-info-btn:hover { + background: #f7f7f7; +} + +.modal-info-btn:active { + transform: scale(0.95); +} + +.modal-info-btn:focus-visible { + outline: 2px solid #4c9ffe; + outline-offset: 2px; +} + + +/* ℹ️ evidenziato quando il pannello info è aperto */ +.modal-info-btn.active { + background: #f7f7f7; + border-color: #cfcfcf; + box-shadow: 0 2px 8px rgba(0,0,0,0.25); + transform: none; +} + +/* =============================== + (OPZIONALE) LINK "APRI ORIGINALE ↗" + =============================== */ + +.modal-open-original { + position: absolute; + top: 8px; + right: 56px; /* lascia spazio alla X */ + background: rgba(255,255,255,0.95); + color: #000; + border-radius: 16px; + height: 32px; + padding: 0 10px; + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + border: 1px solid #d0d0d0; + font-size: 13px; + z-index: 10000; /* sopra al media */ + text-decoration: none; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); +} + +.modal-open-original:hover { + background: #fff; +} + +.modal-open-original:focus-visible { + outline: 2px solid #4c9ffe; + outline-offset: 2px; + border-radius: 6px; +} + +/* =============================== + MODAL STATE UTILI + =============================== */ + +body.no-scroll { + overflow: hidden; +} + +/* High contrast / accessibility (opzionale) */ +@media (prefers-contrast: more) { + .modal { + background: rgba(0,0,0,0.9); + } + .modal-close, + .modal-info-btn, + .modal-open-original { + border-color: #000; + box-shadow: none; + } +} + +/* Riduci animazioni se l’utente lo preferisce */ +@media (prefers-reduced-motion: reduce) { + .modal, + .modal-content { + transition: none !important; + } +} + + + +/* =============================== + FRECCE DI NAVIGAZIONE < > + =============================== */ + +.modal-nav-btn { + position: fixed; /* fisso: resta sopra a video/immagine */ + top: calc(50% + env(safe-area-inset-top)); + transform: translateY(-50%); + z-index: 10000; /* sopra al media, sotto alla X (10001) */ + + width: 44px; + height: 44px; + border-radius: 22px; + border: 1px solid rgba(255,255,255,0.25); + background: rgba(0,0,0,0.35); + color: #fff; + font-size: 22px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; + box-shadow: 0 2px 8px rgba(0,0,0,0.4); + + transition: background-color .15s ease, transform .05s ease; +} + +.modal-nav-btn:hover { background: rgba(0,0,0,0.5); } +.modal-nav-btn:active { transform: translateY(-50%) translateY(1px); } +.modal-nav-btn:focus-visible { + outline: 2px solid #4c9ffe; + outline-offset: 2px; + border-radius: 8px; +} + +.modal-nav-btn.prev { left: calc(12px + env(safe-area-inset-left)); } +.modal-nav-btn.next { right: calc(12px + env(safe-area-inset-right)); } + +/* Nascondi automaticamente se c'è un solo elemento */ +.modal-nav-btn.hidden { display: none !important; } diff --git a/public/css/optionsSheet.css b/public/css/optionsSheet.css new file mode 100644 index 0000000..378d064 --- /dev/null +++ b/public/css/optionsSheet.css @@ -0,0 +1,26 @@ +#optionsSheet .sheet-content { + padding: 20px; +} + +#optionsSheet h3 { + margin-top: 20px; + margin-bottom: 10px; + font-size: 16px; + color: #444; +} + +.sheet-btn { + width: 100%; + padding: 12px; + margin-bottom: 8px; + text-align: left; + background: #f5f5f5; + border: none; + border-radius: 8px; + font-size: 15px; + cursor: pointer; +} + +.sheet-btn:hover { + background: #e8e8e8; +} diff --git a/public/css/utils.css b/public/css/utils.css new file mode 100644 index 0000000..d88c06f --- /dev/null +++ b/public/css/utils.css @@ -0,0 +1,24 @@ +.hidden { + display: none !important; +} + +.rounded { + border-radius: 10px; +} + +.shadow-sm { + box-shadow: 0 1px 3px rgba(0,0,0,0.15); +} + +.shadow-md { + box-shadow: 0 2px 6px rgba(0,0,0,0.25); +} + +.fade-in { + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} diff --git a/public/img/switch.png b/public/img/switch.png new file mode 100644 index 0000000..bede7f7 Binary files /dev/null and b/public/img/switch.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..68aabba --- /dev/null +++ b/public/index.html @@ -0,0 +1,175 @@ + + + + + Galleria Foto + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Galleria Foto

+ +
+ + + + +
+
+ +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+ + + + +
+
+ +
+ + + + +
+
+ +
+ +

Ordinamento

+ + + +

Raggruppamento

+ + + + + +

Filtri

+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/js/bottomSheet.js b/public/js/bottomSheet.js new file mode 100644 index 0000000..8430eec --- /dev/null +++ b/public/js/bottomSheet.js @@ -0,0 +1,133 @@ +// =============================== +// BOTTOM SHEET — strip multi-foto (2+); singola → modal diretto +// Regola: aprendo qualcosa di nuovo, chiudo il precedente +// =============================== + +const bottomSheet = document.getElementById("bottomSheet"); +const sheetGallery = document.getElementById("sheetGallery"); +let optionsSheetRef = document.getElementById("optionsSheet"); + +// Overlay (creazione difensiva) +let sheetOverlay = document.getElementById("sheetOverlay"); +if (!sheetOverlay) { + sheetOverlay = document.createElement("div"); + sheetOverlay.id = "sheetOverlay"; + sheetOverlay.className = "sheet-overlay"; + document.body.appendChild(sheetOverlay); +} + +function openBottomSheet(photoList) { + const list = Array.isArray(photoList) ? photoList : []; + + // 0 o 1 foto → MODAL diretto, nessuna bottom-zone + if (list.length <= 1) { + const p = list[0]; + if (p) { +const thumbUrl = absUrl( + p.thub2 || p.thub1 || p.path, + p.user, + "thumbs", + p.cartella +); + +const originalUrl = absUrl( + p.path, + p.user, + "original", + p.cartella +); + + closeBottomSheet(); + + if (typeof window.openModalFromList === "function") { + window.openModalFromList([p], 0); + } else { + window.openModal?.(originalUrl, thumbUrl, p); + } + } + return; + } + + // 2+ foto → strip in basso + sheetGallery.innerHTML = ""; + + list.forEach((photo, index) => { +const thumbUrl = absUrl( + photo.thub2 || photo.thub1, + photo.user, + "thumbs", + photo.cartella +); + +const originalUrl = absUrl( + photo.path, + photo.user, + "original", + photo.cartella +); + + const div = document.createElement("div"); + div.className = "sheet-item"; + div.tabIndex = 0; + + const img = document.createElement("img"); + img.className = "sheet-thumb"; + img.src = thumbUrl; + img.alt = photo?.name || ""; + img.loading = "lazy"; + + const openFromIndex = () => { + closeBottomSheet(); + if (typeof window.openModalFromList === "function") { + window.openModalFromList(list, index); + } else { + window.openModal?.(originalUrl, thumbUrl, photo); + } + }; + + div.addEventListener("click", openFromIndex); + div.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openFromIndex(); } + }); + + div.appendChild(img); + sheetGallery.appendChild(div); + }); + + bottomSheet.classList.add("open"); + sheetOverlay.classList.add("open"); +} + +function closeBottomSheet() { + bottomSheet.classList.remove("open"); + sheetOverlay.classList.remove("open"); +} + +function openOptionsSheet() { + optionsSheetRef?.classList.add("open"); + sheetOverlay.classList.add("open"); +} +function closeOptionsSheet() { + optionsSheetRef?.classList.remove("open"); + sheetOverlay.classList.remove("open"); +} + +// Chiusura cliccando fuori +sheetOverlay.addEventListener("click", () => { + closeBottomSheet(); + closeOptionsSheet(); +}); + +// Chiusura toccando la maniglia +document.querySelectorAll(".sheet-header").forEach(header => { + header.addEventListener("click", () => { + closeBottomSheet(); + closeOptionsSheet(); + }); +}); + +// Export +window.openBottomSheet = openBottomSheet; +window.closeBottomSheet = closeBottomSheet; +window.openOptionsSheet = openOptionsSheet; +window.closeOptionsSheet = closeOptionsSheet; \ No newline at end of file diff --git a/public/js/config.js b/public/js/config.js new file mode 100644 index 0000000..0e6463b --- /dev/null +++ b/public/js/config.js @@ -0,0 +1,63 @@ +// =============================== +// CONFIG DINAMICA DAL SERVER +// =============================== + +window.BASE_URL = null; +window.PHOTOS_URL = null; +window.MEDIA_BASE_ORIGIN = null; +window.configReady = false; +window.PATH_FULL = false; + +// Carica /config dal backend + +(async () => { + try { + const res = await fetch('/config'); + const cfg = await res.json(); + + window.BASE_URL = cfg.baseUrl; + window.PATH_FULL = cfg.pathFull; + window.PHOTOS_URL = `${window.BASE_URL}/photos`; + window.MEDIA_BASE_ORIGIN = new URL(window.PHOTOS_URL).origin; + + console.log("[config] BASE_URL:", window.BASE_URL); + console.log("[config] PHOTOS_URL:", window.PHOTOS_URL); + console.log("[config] MEDIA_BASE_ORIGIN:", window.MEDIA_BASE_ORIGIN); + console.log("[config] PATH_FULL:", window.PATH_FULL); + + window.configReady = true; + + } catch (err) { + console.error("[config] Errore nel caricamento della config:", err); + } +})(); + + + +// =============================== +// Utility: normalizza URL dei media +// =============================== +function toAbsoluteUrl1(pathOrUrl) { + if (!pathOrUrl) return ''; + if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl; + + const normalized = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`; + return `${window.MEDIA_BASE_ORIGIN}${normalized}`; +} + +function toAbsoluteUrl(pathOrUrl, name, type, cartella) { + const BASE_URL = 'https://prova.patachina.it'; + + if (!pathOrUrl) return ''; + if (!name || !type || !cartella) { + throw new Error('name, type e cartella sono obbligatori'); + } + + // Normalizza il path rimuovendo slash iniziali/finali + const cleanedPath = String(pathOrUrl).replace(/^\/+|\/+$/g, ''); + + // Costruzione dell'URL finale + return `${BASE_URL}/photos/${encodeURIComponent(name)}/${encodeURIComponent(type)}/${encodeURIComponent(cartella)}/${cleanedPath}`; +} + +window.toAbsoluteUrl = toAbsoluteUrl; diff --git a/public/js/data.js b/public/js/data.js new file mode 100644 index 0000000..fc482f6 --- /dev/null +++ b/public/js/data.js @@ -0,0 +1,84 @@ +// =============================== +// FETCH DELLE FOTO +// =============================== +async function loadPhotos() { + console.log("Inizio fetch:", window.PHOTOS_URL); + + let res; + try { + res = await fetch(window.PHOTOS_URL, { + headers: { + "Authorization": "Bearer " + window.token + } + }); + } catch (e) { + console.error("Errore fetch:", e); + return; + } + + const text = await res.text(); + + if (!res.ok) { + console.error(`HTTP ${res.status} ${res.statusText} – body:`, text.slice(0, 200)); + return; + } + + try { + const parsed = JSON.parse(text); + + if (!Array.isArray(parsed)) { + console.error("La risposta non è un array:", parsed); + return; + } + + window.photosData = parsed; + console.log("JSON parse OK, numero foto:", parsed.length); + + } catch (e) { + console.error("Errore nel parse JSON:", e); + return; + } + + refreshGallery(); +} + +async function loadPhotos1() { + console.log("Inizio fetch:", window.BASE_URL + "/photos"); + + let res; + try { + res = await fetch(`${window.BASE_URL}/photos`, { + headers: { + "Authorization": "Bearer " + window.token + } + }); + } catch (e) { + console.error("Errore fetch:", e); + return; + } + + const text = await res.text(); + + if (!res.ok) { + console.error(`HTTP ${res.status} ${res.statusText} – body:`, text.slice(0, 200)); + return; + } + + try { + const parsed = JSON.parse(text); + + if (!Array.isArray(parsed)) { + console.error("La risposta non è un array:", parsed); + return; + } + + window.photosData = parsed; + console.log("JSON parse OK, numero foto:", parsed.length); + + } catch (e) { + console.error("Errore nel parse JSON:", e); + return; + } + + refreshGallery(); +} diff --git a/public/js/gallery.js b/public/js/gallery.js new file mode 100644 index 0000000..1054b8b --- /dev/null +++ b/public/js/gallery.js @@ -0,0 +1,165 @@ +// =============================== +// GALLERY — completa, stile Google Photos +// - Ordinamento +// - Filtri +// - Raggruppamento (auto/giorno/mese/anno) +// - Render a sezioni +// - Click: openModalFromList(sezione, indice) se disponibile (fallback openModal) +// =============================== + +// ORDINAMENTO +function sortByDate(photos, direction = "desc") { + return photos.slice().sort((a, b) => { + const da = a?.taken_at ? new Date(a.taken_at) : 0; + const db = b?.taken_at ? new Date(b.taken_at) : 0; + return direction === "asc" ? (da - db) : (db - da); + }); +} + +// FILTRI +function applyFilters(photos) { + if (!window.currentFilter) return photos; + + switch (window.currentFilter) { + case "folder": + return photos.filter(p => p.folder || (p.path && p.path.includes('/photos/'))); + case "location": + return photos.filter(p => p?.gps && p.gps.lat); + case "type": + return photos.filter(p => p?.mime_type && p.mime_type.startsWith("image/")); + default: + return photos; + } +} + +// RAGGRUPPAMENTO STILE GOOGLE PHOTOS +function groupByDate(photos, mode = "auto") { + const sections = []; + const now = new Date(); + + function getLabel(photo) { + const date = photo?.taken_at ? new Date(photo.taken_at) : null; + if (!date || isNaN(+date)) return "Senza data"; + + const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); + if (mode === "day") return formatDay(date); + if (mode === "month") return formatMonth(date); + if (mode === "year") return date.getFullYear().toString(); + + if (diffDays === 0) return "Oggi"; + if (diffDays === 1) return "Ieri"; + if (diffDays <= 7) return "Questa settimana"; + if (diffDays <= 14) return "La settimana scorsa"; + if (diffDays <= 30) return "Questo mese"; + if (diffDays <= 60) return "Mese scorso"; + + if (date.getFullYear() === now.getFullYear()) return formatMonth(date); + return date.getFullYear().toString(); + } + + photos.forEach(photo => { + const label = getLabel(photo); + let section = sections.find(s => s.label === label); + if (!section) { + section = { label, photos: [] }; + sections.push(section); + } + section.photos.push(photo); + }); + + return sections; +} + +// FORMATTATORI +function formatDay(date) { + return date.toLocaleDateString("it-IT", { weekday: "long", day: "numeric", month: "long" }); +} +function formatMonth(date) { + return date.toLocaleDateString("it-IT", { month: "long", year: "numeric" }); +} + +// RENDER +function renderGallery(sections) { + const gallery = document.getElementById("gallery"); + if (!gallery) return; + gallery.innerHTML = ""; + + sections.forEach(section => { + const h = document.createElement("h2"); + h.className = "gallery-section-title"; + h.textContent = section.label; + gallery.appendChild(h); + + const container = document.createElement("div"); + container.className = "gallery-section"; + + section.photos.forEach((photo, idx) => { + const thumbDiv = document.createElement("div"); + thumbDiv.className = "thumb"; + +// const th1 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub1) : photo?.thub1; +// const th2 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub2 || photo?.thub1) : (photo?.thub2 || photo?.thub1); +// const original = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.path) : photo?.path; + +let th1, th2, original; + +if (window.PATH_FULL) { + // Uso direttamente i path completi generati dal backend + th1 = photo?.thub1; + th2 = photo?.thub2 || photo?.thub1; + original = photo?.path; +} else { + // Comportamento attuale: costruisco URL con toAbsoluteUrl() + th1 = (typeof toAbsoluteUrl === 'function') + ? toAbsoluteUrl(photo?.thub1, photo?.user, "thumbs", photo?.cartella) + : photo?.thub1; + + th2 = (typeof toAbsoluteUrl === 'function') + ? toAbsoluteUrl(photo?.thub2 || photo?.thub1, photo?.user, "thumbs", photo?.cartella) + : (photo?.thub2 || photo?.thub1); + + original = (typeof toAbsoluteUrl === 'function') + ? toAbsoluteUrl(photo?.path, photo?.user, "original", photo?.cartella) + : photo?.path; +} + + +//console.log(photo?.user); +console.log(th1); + + const img = document.createElement("img"); + img.src = th1 || th2 || original || ""; + img.alt = photo?.name || ""; + img.loading = "lazy"; + thumbDiv.appendChild(img); + + if (photo?.mime_type && photo.mime_type.startsWith("video/")) { + const play = document.createElement("div"); + play.className = "play-icon"; + play.textContent = "▶"; + thumbDiv.appendChild(play); + } + + thumbDiv.addEventListener("click", () => { + // Chiudi sempre la strip prima di aprire una nuova foto + window.closeBottomSheet?.(); + + if (typeof window.openModalFromList === "function") { + window.openModalFromList(section.photos, idx); + } else { + window.openModal?.(original, th2, photo); + } + }); + + container.appendChild(thumbDiv); + }); + + gallery.appendChild(container); + }); +} + +// Esporti su window +window.sortByDate = sortByDate; +window.applyFilters = applyFilters; +window.groupByDate = groupByDate; +window.renderGallery = renderGallery; diff --git a/public/js/infoPanel.js b/public/js/infoPanel.js new file mode 100644 index 0000000..11ca303 --- /dev/null +++ b/public/js/infoPanel.js @@ -0,0 +1,167 @@ +// =============================== +// PANNELLO INFO + MAPPA (toggle affidabile + auto-refresh su cambio foto) +// =============================== + +const infoPanel = document.getElementById('infoPanel'); +let infoMapInstance = null; // tieni traccia della mappa per pulizia + +// ------------------------------- +// Helpers UI / stato +// ------------------------------- +function isPanelOpen() { + return infoPanel.classList.contains('open') || + infoPanel.getAttribute('aria-hidden') === 'false' || + infoPanel.getAttribute('data-open') === '1' || + infoPanel.style.display === 'block'; +} + +function markButtonActive(active) { + const btn = document.getElementById('modalInfoBtn'); + if (btn) btn.classList.toggle('active', !!active); +} + +// ------------------------------- +// Render contenuti + (ri)creazione mappa +// ------------------------------- +function renderInfo(photo) { + if (!photo) return; + + const gps = photo.gps || { lat: '-', lng: '-', alt: '-' }; + const folder = photo.path?.split('/').slice(2, -1).join('/') || '-'; + const loc = photo.location || {}; + + // Inietta contenuti + infoPanel.innerHTML = ` +

Informazioni

+ +
Nome: ${photo.name ?? '-'}
+
Data: ${photo.taken_at ?? '-'}
+ +
Latitudine: ${gps.lat ?? '-'}
+
Longitudine: ${gps.lng ?? '-'}
+
Altitudine: ${gps.alt ?? '-'} m
+ +
Dimensioni: ${photo.width ?? '-'} × ${photo.height ?? '-'}
+
Peso: ${photo.size_bytes ? (photo.size_bytes / 1024 / 1024).toFixed(2) + ' MB' : '-'}
+
Tipo: ${photo.mime_type ?? '-'}
+ +
Cartella: ${folder}
+ +
+ +

Mappa

+ ${gps.lat !== '-' && gps.lng !== '-' ? '
' : ''} + +
+ +

Location

+ ${loc.continent ? `
Continente: ${loc.continent}
` : ''} + ${loc.country ? `
Nazione: ${loc.country}
` : ''} + ${loc.region ? `
Regione: ${loc.region}
` : ''} + ${loc.city ? `
Città: ${loc.city}
` : ''} + ${loc.address ? `
Indirizzo: ${loc.address}
` : ''} + ${loc.postcode ? `
CAP: ${loc.postcode}
` : ''} + ${loc.county_code ? `
Provincia: ${loc.county_code}
` : ''} + ${loc.timezone ? `
Timezone: ${loc.timezone}
` : ''} + ${loc.time ? `
Offset: ${loc.time}
` : ''} + `; + + // (Ri)crea la mappa se ci sono coordinate + // 1) Pulisci istanza precedente (evita "Map container is already initialized") + try { infoMapInstance?.remove(); } catch {} + infoMapInstance = null; + + if (gps.lat !== '-' && gps.lng !== '-') { + setTimeout(() => { + try { + infoMapInstance = L.map('infoMap', { + zoomControl: false, + attributionControl: false + }).setView([gps.lat, gps.lng], 13); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19 + }).addTo(infoMapInstance); + + L.marker([gps.lat, gps.lng]).addTo(infoMapInstance); + } catch (err) { + console.warn('Errore creazione mappa info:', err); + } + }, 50); + } +} + +// ------------------------------- +// API pubbliche: apri / chiudi / toggle +// ------------------------------- +window.openInfoPanel = function openInfoPanel(photo) { + renderInfo(photo || window.currentPhoto); + infoPanel.classList.add('open'); + infoPanel.setAttribute('aria-hidden', 'false'); + infoPanel.setAttribute('data-open', '1'); + markButtonActive(true); +}; + +window.closeInfoPanel = function closeInfoPanel() { + infoPanel.classList.remove('open'); + infoPanel.setAttribute('aria-hidden', 'true'); + infoPanel.setAttribute('data-open', '0'); + markButtonActive(false); + + // pulizia mappa + try { infoMapInstance?.remove(); } catch {} + infoMapInstance = null; +}; + +window.toggleInfoPanel = function toggleInfoPanel(photo) { + if (isPanelOpen()) window.closeInfoPanel(); + else window.openInfoPanel(photo || window.currentPhoto); +}; + +// ------------------------------- +// Delegation: click su ℹ️ = TOGGLE vero +// ------------------------------- +document.addEventListener('click', (e) => { + if (e.target.id !== 'modalInfoBtn') return; + e.stopPropagation(); // evita side-effects (es. navigazione ai bordi) + window.toggleInfoPanel(window.currentPhoto); +}); + +// Chiudi pannello cliccando FUORI (non su ℹ️) +document.addEventListener('click', (e) => { + if (!isPanelOpen()) return; + const inside = infoPanel.contains(e.target); + const isBtn = e.target.id === 'modalInfoBtn'; + if (!inside && !isBtn) window.closeInfoPanel(); +}); + +// ------------------------------- +/* Auto-refresh: se cambia il media nel modal e l'info è aperto, aggiorna */ +(() => { + const mediaContainer = document.getElementById('modalMediaContainer'); + if (!mediaContainer) return; + + const refreshIfOpen = () => { + if (!isPanelOpen()) return; + const photo = window.currentPhoto; + if (photo) renderInfo(photo); + }; + + // 1) Osserva la sostituzione del media (immagine/video) nel modal + const mo = new MutationObserver(() => { + // Debounce minimo per evitare doppi render durante il replace + setTimeout(refreshIfOpen, 0); + }); + mo.observe(mediaContainer, { childList: true }); + + // 2) (Extra robustezza) ascolta le frecce se esistono + document.getElementById('modalPrev')?.addEventListener('click', () => setTimeout(refreshIfOpen, 0)); + document.getElementById('modalNext')?.addEventListener('click', () => setTimeout(refreshIfOpen, 0)); + + // 3) (Extra) tastiera ←/→ + document.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { + setTimeout(refreshIfOpen, 0); + } + }); +})(); \ No newline at end of file diff --git a/public/js/logout.js b/public/js/logout.js new file mode 100644 index 0000000..d01d1a0 --- /dev/null +++ b/public/js/logout.js @@ -0,0 +1,153 @@ +// public/js/logout.js +// Gestione logout: chiama /auth/logout, pulisce i token, redirige e programma l'auto-logout a scadenza JWT. +// Espone: window.AppAuth.logout({redirect: true|false}) e window.AppAuth.isLoggedIn(). + +(() => { + const AUTH_LOGOUT_ENDPOINT = '/auth/logout'; + const KEYS = ['access_token', 'token', 'refresh_token']; // intercetta sia 'token' che 'access_token' + + // --- Helpers token -------------------------------------------------------- + function getAccessToken() { + try { + return ( + localStorage.getItem('access_token') || + localStorage.getItem('token') || + '' + ); + } catch { + return ''; + } + } + + function clearTokens() { + try { + KEYS.forEach(k => localStorage.removeItem(k)); + // Se usi sessionStorage per altro, rimuovi solo chiavi auth (non fare clear totale) + // sessionStorage.removeItem('my_auth_state'); // esempio + } catch {} + } + + // --- Chiamata server ------------------------------------------------------ + async function serverLogout(token) { + try { + const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + await fetch(AUTH_LOGOUT_ENDPOINT, { method: 'POST', headers }); + } catch (err) { + // In caso di errore rete, lato client puliamo comunque. + console.warn('Logout server fallito (ignoro):', err); + } + } + + // --- Redirect ------------------------------------------------------------- + function getRedirectFromBtn() { + const btn = document.querySelector('[data-logout]'); + return btn?.getAttribute('data-redirect') || '/'; + } + + function redirectAfterLogout(redirectUrl) { + const target = redirectUrl || '/'; + window.location.assign(target); + } + + // --- Auto-logout alla scadenza JWT --------------------------------------- + function decodeJwt(token) { + try { + const base64Url = token.split('.')[1]; + if (!base64Url) return null; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('') + ); + return JSON.parse(jsonPayload); + } catch { + return null; + } + } + + let autoLogoutTimer = null; + function scheduleAutoLogout() { + clearTimeout(autoLogoutTimer); + const token = getAccessToken(); + if (!token) return; + + const payload = decodeJwt(token); + const expSec = payload?.exp; + if (!expSec) return; + + const msToExp = expSec * 1000 - Date.now(); + if (msToExp <= 0) { + // già scaduto → logout immediato + doLogout({ redirect: true }); + return; + } + + autoLogoutTimer = setTimeout(() => { + doLogout({ redirect: true }); + }, msToExp); + } + + // --- UI helpers ----------------------------------------------------------- + function setButtonsDisabled(disabled) { + document.querySelectorAll('[data-logout]').forEach(btn => { + btn.disabled = disabled; + btn.setAttribute('aria-busy', disabled ? 'true' : 'false'); + }); + } + + // --- Azione principale ---------------------------------------------------- + async function doLogout({ redirect = true } = {}) { + const token = getAccessToken(); + setButtonsDisabled(true); + + // 1) Revoca lato server (denylist) se possibile + await serverLogout(token); + + // 2) Pulizia lato client + clearTokens(); + + // 3) Chiudi eventuali media globali + try { window.player?.pause?.(); } catch {} + + // 4) Notifica globale (se vuoi ascoltarla altrove) + try { window.dispatchEvent(new CustomEvent('logout:success')); } catch {} + + // 5) Redirect + if (redirect) redirectAfterLogout(getRedirectFromBtn()); + + setButtonsDisabled(false); + } + + // --- Click handler -------------------------------------------------------- + function bindLogoutButtons() { + document.querySelectorAll('[data-logout]').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.preventDefault(); + if (btn.disabled) return; // evita doppi click + await doLogout({ redirect: true }); + }); + }); + } + + // --- Public API ----------------------------------------------------------- + window.AppAuth = Object.freeze({ + logout: (opts) => doLogout(opts), + isLoggedIn: () => !!getAccessToken() + }); + + // --- Init ----------------------------------------------------------------- + function init() { + bindLogoutButtons(); + scheduleAutoLogout(); + + // Opzionale: nascondi il bottone se non loggato + document.querySelectorAll('[data-logout]').forEach(btn => { + if (!window.AppAuth.isLoggedIn()) btn.style.display = 'none'; + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..4126e98 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,293 @@ +// =============================== +// AVVIO +// =============================== +console.log("main.js avviato"); + +// =============================== +// UTILS AUTH + SYNC HEADER UI +// =============================== +function isAuthenticated() { + // Fonte verità: presenza del token in localStorage + return !!localStorage.getItem("token"); +} + +/** + * Sincronizza l’UI dell’header in base allo stato auth: + * - Aggiunge una classe sul body (CSS-friendly) + * - Mostra/Nasconde il bottone logout (hotfix inline, se vuoi puoi affidarti solo al CSS) + */ +function syncHeaderAuthUI() { + const authed = isAuthenticated(); + document.body.classList.toggle('authenticated', authed); + + const logoutBtn = document.getElementById('logoutBtn'); + if (logoutBtn) { + // Hotfix immediato: forza la visibilità anche via inline style + // (puoi rimuovere queste due righe se preferisci usare solo la regola CSS body.authenticated #logoutBtn) + logoutBtn.style.display = authed ? 'inline-flex' : 'none'; + } +} + +// =============================== +// PATCH: misura l'altezza reale dell'header e aggiorna --header-h +// (serve per far partire la mappa subito sotto l’header, anche su mobile) +// =============================== +(function () { + const root = document.documentElement; + const header = document.querySelector('header'); + + function setHeaderHeight() { + const h = header ? Math.round(header.getBoundingClientRect().height) : 60; + root.style.setProperty('--header-h', h + 'px'); + } + + setHeaderHeight(); + + if (window.ResizeObserver && header) { + const ro = new ResizeObserver(setHeaderHeight); + ro.observe(header); + } else { + window.addEventListener('resize', setHeaderHeight); + window.addEventListener('orientationchange', setHeaderHeight); + } + + window.addEventListener('load', setHeaderHeight); +})(); + +// =============================== +// PATCH: quando si apre la mappa (#globalMap.open) invalida le dimensioni Leaflet +// (utile perché prima era display:none; invalidateSize evita “tagli” o tile sfasati) +// =============================== +(function () { + const mapEl = document.getElementById('globalMap'); + if (!mapEl) return; + + function invalidateWhenOpen() { + if (!mapEl.classList.contains('open')) return; + // Aspetta un tick così il layout è aggiornato + setTimeout(() => { + try { + // In mapGlobal.js imposta: window.leafletMapInstance = window.globalMap; + window.leafletMapInstance?.invalidateSize(); + } catch (e) { + console.warn('invalidateSize non eseguito:', e); + } + }, 0); + } + + // 1) Osserva il cambio classe (quando aggiungi .open) + const mo = new MutationObserver((mutations) => { + if (mutations.some(m => m.attributeName === 'class')) { + invalidateWhenOpen(); + } + }); + mo.observe(mapEl, { attributes: true, attributeFilter: ['class'] }); + + // 2) Fallback: se usi il bottone #openMapBtn per aprire/chiudere + document.getElementById('openMapBtn')?.addEventListener('click', () => { + setTimeout(invalidateWhenOpen, 0); + }); +})(); + +// =============================== +// MENU ⋮ (toggle apri/chiudi con lo stesso bottone, senza global conflicts) +// =============================== +(() => { + const optBtn = document.getElementById("optionsBtn"); + const optSheet = document.getElementById("optionsSheet"); + const overlayEl= document.getElementById("sheetOverlay"); + + if (!optBtn || !optSheet) return; + + function openOptionsSheet() { + try { window.closeBottomSheet?.(); } catch {} + optSheet.classList.add("open"); + overlayEl?.classList.add("open"); + // ARIA (facoltativo) + optBtn.setAttribute("aria-expanded", "true"); + optSheet.setAttribute("aria-hidden", "false"); + } + + function closeOptionsSheet() { + optSheet.classList.remove("open"); + overlayEl?.classList.remove("open"); + // ARIA (facoltativo) + optBtn.setAttribute("aria-expanded", "false"); + optSheet.setAttribute("aria-hidden", "true"); + } + + function toggleOptionsSheet(e) { + e?.preventDefault(); + e?.stopPropagation(); + if (optSheet.classList.contains("open")) closeOptionsSheet(); + else openOptionsSheet(); + } + + // Click sul bottone: toggle (fase di cattura per battere eventuali altri handler) + optBtn.addEventListener("click", toggleOptionsSheet, { capture: true }); + + // Chiudi clic overlay + overlayEl?.addEventListener("click", (e) => { + e.stopPropagation(); + closeOptionsSheet(); + }); + + // Chiudi con ESC + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && optSheet.classList.contains("open")) { + closeOptionsSheet(); + } + }); + + // Evita chiusure involontarie per click interni + optSheet.addEventListener("click", (e) => e.stopPropagation()); + + // Espone una close per usarla altrove (es. dopo la scelta) + window.closeOptionsSheet = closeOptionsSheet; +})(); + +// =============================== +// LOGIN AUTOMATICO SU INDEX +// =============================== +document.addEventListener("DOMContentLoaded", async () => { + // Allinea subito l’UI in base al token eventualmente già presente + syncHeaderAuthUI(); + + try { + // 1) Carica config + const cfgRes = await fetch('/config'); + const cfg = await cfgRes.json(); + window.BASE_URL = cfg.baseUrl; + + // 2) Recupera token salvato + const savedToken = localStorage.getItem("token"); + + // Se non c'è token → mostra login + if (!savedToken) { + document.getElementById("loginModal").style.display = "flex"; + return; + } + + // 3) Verifica token + const ping = await fetch(`${window.BASE_URL}/photos`, { + headers: { "Authorization": "Bearer " + savedToken } + }); + + if (!ping.ok) { + // Token invalido → cancella e mostra login + localStorage.removeItem("token"); + syncHeaderAuthUI(); // riallinea header subito + document.getElementById("loginModal").style.display = "flex"; + return; + } + + // 4) Token valido → salva e carica gallery + window.token = savedToken; + syncHeaderAuthUI(); // <— mostra il logout senza refresh + loadPhotos(); + + } catch (err) { + console.error("Errore autenticazione:", err); + document.getElementById("loginModal").style.display = "flex"; + } +}); + +// =============================== +// VARIABILI GLOBALI +// =============================== +let currentSort = "desc"; +let currentGroup = "auto"; +let currentFilter = null; + +window.currentSort = currentSort; +window.currentGroup = currentGroup; +window.currentFilter = currentFilter; + +// =============================== +// BOTTONI OPZIONI +// =============================== +document.querySelectorAll("#optionsSheet .sheet-btn").forEach(btn => { + btn.addEventListener("click", () => { + if (btn.dataset.sort) window.currentSort = currentSort = btn.dataset.sort; + if (btn.dataset.group) window.currentGroup = currentGroup = btn.dataset.group; + if (btn.dataset.filter) window.currentFilter = currentFilter = btn.dataset.filter; + + // Chiudi sheet e overlay dopo la scelta (usa l’API esposta sopra) + window.closeOptionsSheet?.(); + + refreshGallery(); + }); +}); + +// =============================== +// REFRESH GALLERY +// =============================== +function refreshGallery() { + console.log("Aggiornamento galleria..."); + + const data = Array.isArray(window.photosData) ? window.photosData : []; + let photos = [...data]; + + if (typeof applyFilters === 'function') photos = applyFilters(photos); + if (typeof sortByDate === 'function') photos = sortByDate(photos, currentSort); + + let sections = [{ label: 'Tutte', photos }]; + if (typeof groupByDate === 'function') sections = groupByDate(photos, currentGroup); + + if (typeof renderGallery === 'function') { + renderGallery(sections); + } +} + +window.refreshGallery = refreshGallery; + +// =============================== +// SETTINGS (⚙️) — apre admin.html +// =============================== +const settingsBtn = document.getElementById('settingsBtn'); +settingsBtn?.addEventListener('click', () => { + window.location.href = "admin.html"; +}); + +// =============================== +// LOGIN SUBMIT +// =============================== +document.getElementById("loginSubmit").addEventListener("click", async () => { + const email = document.getElementById("loginEmail").value; + const password = document.getElementById("loginPassword").value; + const errorEl = document.getElementById("loginError"); + + errorEl.textContent = ""; + + try { + const res = await fetch(`${window.BASE_URL}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + if (!res.ok) { + errorEl.textContent = "Utente o password errati"; + return; + } + + const data = await res.json(); + const token = data.token; + + // Salva token + localStorage.setItem("token", token); + window.token = token; + + // Chiudi login + const loginModalEl = document.getElementById("loginModal"); + if (loginModalEl) loginModalEl.style.display = "none"; + + // Riallinea UI header subito (mostra logout) e carica gallery + syncHeaderAuthUI(); + loadPhotos(); + + } catch (err) { + console.error("Errore login:", err); + errorEl.textContent = "Errore di connessione al server"; + } +}); \ No newline at end of file diff --git a/public/js/mapGlobal.js b/public/js/mapGlobal.js new file mode 100644 index 0000000..a3e84cc --- /dev/null +++ b/public/js/mapGlobal.js @@ -0,0 +1,250 @@ +// =============================== +// MAPPA GLOBALE — stile Google Photos Web +// =============================== + +window.globalMap = null; +window.globalMarkers = null; // qui sarà un MarkerClusterGroup + +document.addEventListener("DOMContentLoaded", () => { + const openBtn = document.getElementById("openMapBtn"); + if (!openBtn) { + console.error("openMapBtn non trovato nel DOM"); + return; + } + openBtn.addEventListener("click", openGlobalMap); + + const RADIUS_PX = 50; + const DISABLE_CLUSTER_AT_ZOOM = 18; + const OPEN_STRIP_CHILDREN_MAX = 20; + + async function openGlobalMap() { + const mapDiv = document.getElementById("globalMap"); + const gallery = document.getElementById("gallery"); + if (!mapDiv) { + console.error("globalMap DIV non trovato"); + return; + } + + const isOpen = mapDiv.classList.contains("open"); + + if (isOpen) { + mapDiv.classList.remove("open"); + gallery?.classList.remove("hidden"); + window.closeBottomSheet?.(); + return; + } + + mapDiv.classList.add("open"); + gallery?.classList.add("hidden"); + + if (window.globalMap) { + window.leafletMapInstance = window.globalMap; + } + + await new Promise(r => requestAnimationFrame(r)); + let tries = 0; + while (mapDiv.getBoundingClientRect().height < 50 && tries < 10) { + await new Promise(r => setTimeout(r, 30)); + tries++; + } + + if (window.globalMap === null) { + console.log("Inizializzo mappa Leaflet + MarkerCluster…"); + + window.globalMap = L.map("globalMap", { + zoomControl: true, + attributionControl: true + }).setView([42.5, 12.5], 6); + + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19 + }).addTo(window.globalMap); + + // CLUSTER con iconCreateFunction in stile Google Photos + window.globalMarkers = L.markerClusterGroup({ + showCoverageOnHover: false, + spiderfyOnMaxZoom: true, + disableClusteringAtZoom: DISABLE_CLUSTER_AT_ZOOM, + iconCreateFunction: function(cluster) { + const count = cluster.getChildCount(); + + // dimensione scalata: base + sqrt(count) * fattore, con cap massimo + const size = Math.min(92, Math.round(28 + Math.sqrt(count) * 6)); + + // classi per stili (piccolo/medio/grande) + const cls = count > 200 ? 'cluster-xl' : (count > 50 ? 'cluster-lg' : (count > 10 ? 'cluster-md' : 'cluster-sm')); + + // prendi fino a 4 thumbnails dai child markers (se disponibili) + const children = cluster.getAllChildMarkers().slice(0, 4); + const thumbs = children + .map(m => m.__photo?.thub2 || m.__photo?.thub1) + .filter(Boolean); + + // se esiste toAbsoluteUrl, trasformiamo le thumb in URL assoluti + const resolvedThumbs = (typeof toAbsoluteUrl === "function") + ? thumbs.map(t => absUrl(t, children[0]?.__photo?.user, "thumbs", children[0]?.__photo?.cartella)) + : thumbs; + + // crea collage HTML: fino a 4 immagini in griglia 2x2 + const imgsHtml = resolvedThumbs.length + ? `
${resolvedThumbs.map((t,i)=>`
`).join('')}
` + : ''; + + const html = ` +
+ ${imgsHtml} +
${count}
+
`; + + return L.divIcon({ + html, + className: 'marker-cluster-wrapper', + iconSize: L.point(size, size) + }); + } + }); + + // Listener "clusterclick": zooma oppure, quando pochi, apri strip + window.globalMarkers.on("clusterclick", (a) => { + const childMarkers = a.layer.getAllChildMarkers(); + const count = childMarkers.length; + + if (count <= OPEN_STRIP_CHILDREN_MAX || window.globalMap.getZoom() >= DISABLE_CLUSTER_AT_ZOOM - 1) { + const photos = childMarkers + .map(m => m.__photo) + .filter(Boolean); + + if (photos.length > 1) { + window.openBottomSheet?.(photos); + } else if (photos.length === 1) { + openPhotoModal(photos[0]); + } + } else { + window.globalMap.fitBounds(a.layer.getBounds(), { padding: [60, 60], maxZoom: DISABLE_CLUSTER_AT_ZOOM, animate: true }); + } + }); + + window.globalMap.addLayer(window.globalMarkers); + + // Disegna i marker + redrawPhotoMarkers(); + } + + setTimeout(() => window.globalMap?.invalidateSize?.(), 120); + } + + function createPhotoIcon(photo) { + const thumb = (typeof toAbsoluteUrl === "function") + ? absUrl( + photo?.thub2 || photo?.thub1, + photo?.user, + "thumbs", + photo?.cartella + ) + : (photo?.thub2 || photo?.thub1); + + return L.icon({ + iconUrl: thumb || "", + iconSize: [56, 56], + iconAnchor: [28, 28], + className: "photo-marker" + }); + } + + function openPhotoModal(photo) { + const thumb = (typeof toAbsoluteUrl === "function") + ? absUrl( + photo?.thub2 || photo?.thub1 || photo?.path, + photo?.user, + "thumbs", + photo?.cartella + ) + : (photo?.thub2 || photo?.thub1 || photo?.path); + + const original = (typeof toAbsoluteUrl === "function") + ? absUrl( + photo?.path, + photo?.user, + "original", + photo?.cartella + ) + : photo?.path; + + window.closeBottomSheet?.(); + window.openModal?.(original, thumb, photo); + } + + function radiusMetersAtZoom(latlng, px) { + if (!window.globalMap) return 0; + const p = window.globalMap.latLngToContainerPoint(latlng); + const p2 = L.point(p.x + px, p.y); + const ll2 = window.globalMap.containerPointToLatLng(p2); + return window.globalMap.distance(latlng, ll2); + } + + function distanceMeters(lat1, lng1, lat2, lng2) { + const toRad = d => d * Math.PI / 180; + const R = 6371000; + const φ1 = toRad(lat1), φ2 = toRad(lat2); + const dφ = toRad(lat2 - lat1); + const dλ = toRad(lng2 - lng1); + const a = Math.sin(dφ/2) ** 2 + + Math.cos(φ1) * Math.cos(φ2) * + Math.sin(dλ/2) ** 2; + return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + } + + function buildGroupByRadius(lat, lng, data, radiusM) { + return data.filter(p => { + const plat = +p?.gps?.lat; + const plng = +p?.gps?.lng; + if (!plat || !plng) return false; + return distanceMeters(lat, lng, plat, plng) <= radiusM; + }); + } + + function redrawPhotoMarkers() { + if (!window.globalMarkers || !window.globalMap) return; + + window.globalMarkers.clearLayers(); + const data = Array.isArray(window.photosData) ? window.photosData : []; + + data.forEach(photo => { + const lat = +photo?.gps?.lat; + const lng = +photo?.gps?.lng; + if (!lat || !lng) return; + + const marker = L.marker([lat, lng], { + icon: createPhotoIcon(photo), + title: photo?.name || "" + }); + + marker.__photo = photo; + + marker.on("click", () => { + const here = L.latLng(lat, lng); + const radiusM = radiusMetersAtZoom(here, RADIUS_PX); + const dataAll = Array.isArray(window.photosData) ? window.photosData : []; + const group = buildGroupByRadius(lat, lng, dataAll, radiusM); + + if (group.length > 1) { + window.openBottomSheet?.(group); + } else if (group.length === 1) { + openPhotoModal(group[0]); + } else { + openPhotoModal(photo); + } + }); + + window.globalMarkers.addLayer(marker); + }); + } + + const originalRefresh = window.refreshGallery; + window.refreshGallery = function wrappedRefreshGallery(...args) { + try { originalRefresh?.apply(this, args); } catch (_) {} + if (window.globalMap && window.globalMarkers) { + redrawPhotoMarkers(); + } + }; +}); diff --git a/public/js/modal.js b/public/js/modal.js new file mode 100644 index 0000000..e2d4c41 --- /dev/null +++ b/public/js/modal.js @@ -0,0 +1,378 @@ +// =============================== +// MODALE (FOTO + VIDEO) — avanzato con navigazione e preload +// - Sostituisce il contenuto, non accumula +// - Chiude la bottom-zone quando si apre +// - Prev/Next (←/→ e click ai bordi), preload 3+3 +// - Pulsante INFO (ℹ️) riportato dentro il modal con toggle affidabile +// =============================== + +const modal = document.getElementById('modal'); +const modalClose = document.getElementById('modalClose'); + +window.currentPhoto = null; // usato anche da infoPanel +window.modalList = []; // lista corrente per navigazione +window.modalIndex = 0; // indice corrente nella lista + +// Frecce visibili +const modalPrev = document.getElementById('modalPrev'); +const modalNext = document.getElementById('modalNext'); + +// =============================== +// Stato/Helper Info Panel (toggle affidabile) +// =============================== +let infoOpen = false; // stato interno affidabile + +function getInfoPanel() { + return document.getElementById('infoPanel'); +} + +function isInfoOpen() { + return infoOpen; +} + +function openInfo(photo) { + // Prova API esplicita, altrimenti fallback a toggle + try { + if (typeof window.openInfoPanel === 'function') { + window.openInfoPanel(photo); + } else if (typeof window.toggleInfoPanel === 'function') { + window.toggleInfoPanel(photo); + } + } catch {} + + infoOpen = true; + const panel = getInfoPanel(); + panel?.classList.add('open'); + panel?.setAttribute('aria-hidden', 'false'); + panel?.setAttribute('data-open', '1'); + document.getElementById('modalInfoBtn')?.classList.add('active'); +} + +function closeInfo() { + // Prova API esplicita, altrimenti fallback a toggle (senza argomento) + try { + if (typeof window.closeInfoPanel === 'function') { + window.closeInfoPanel(); + } else if (typeof window.toggleInfoPanel === 'function') { + window.toggleInfoPanel(); + } + } catch {} + + infoOpen = false; + const panel = getInfoPanel(); + panel?.classList.remove('open'); + panel?.setAttribute('aria-hidden', 'true'); + panel?.setAttribute('data-open', '0'); + document.getElementById('modalInfoBtn')?.classList.remove('active'); +} + +function toggleInfo(photo) { + if (isInfoOpen()) closeInfo(); + else openInfo(photo); +} + +// =============================== +// Utility MIME / media +// =============================== +function isProbablyVideo(photo, srcOriginal) { + const mime = String(photo?.mime_type || '').toLowerCase(); + if (mime.startsWith('video/')) return true; + return /\.(mp4|m4v|webm|mov|qt|avi|mkv)$/i.test(String(srcOriginal || '')); +} + +function guessVideoMime(photo, srcOriginal) { + let t = String(photo?.mime_type || '').toLowerCase(); + if (t && t !== 'application/octet-stream') return t; + const src = String(srcOriginal || ''); + if (/\.(mp4|m4v)$/i.test(src)) return 'video/mp4'; + if (/\.(webm)$/i.test(src)) return 'video/webm'; + if (/\.(mov|qt)$/i.test(src)) return 'video/quicktime'; + if (/\.(avi)$/i.test(src)) return 'video/x-msvideo'; + if (/\.(mkv)$/i.test(src)) return 'video/x-matroska'; + return ''; +} + +function createVideoElement(srcOriginal, srcPreview, photo) { + const video = document.createElement('video'); + video.controls = true; + video.playsInline = true; // iOS: evita fullscreen nativo + video.setAttribute('webkit-playsinline', ''); // compat iOS storici + video.preload = 'metadata'; + video.poster = srcPreview || ''; + video.style.maxWidth = '100%'; + video.style.maxHeight = '100%'; + video.style.objectFit = 'contain'; + + const source = document.createElement('source'); + source.src = srcOriginal; + const type = guessVideoMime(photo, srcOriginal); + if (type) source.type = type; + video.appendChild(source); + + video.addEventListener('loadedmetadata', () => { + try { video.currentTime = 0.001; } catch (_) {} + console.log('[video] loadedmetadata', { w: video.videoWidth, h: video.videoHeight, dur: video.duration }); + }); + + video.addEventListener('error', () => { + const code = video.error && video.error.code; + console.warn('[video] error code:', code, 'type:', type, 'src:', srcOriginal); + const msg = document.createElement('div'); + msg.style.padding = '12px'; + msg.style.color = '#fff'; + msg.style.background = 'rgba(0,0,0,0.6)'; + msg.style.borderRadius = '8px'; + msg.innerHTML = ` + Impossibile riprodurre questo video nel browser. + ${code === 4 ? 'Formato/codec non supportato (es. HEVC/H.265 su Chrome/Edge).' : 'Errore durante il caricamento.'} +

+ Suggerimenti: + + `; + const container = document.getElementById('modalMediaContainer'); + container && container.appendChild(msg); + }); + + // Evita di far scattare la navigazione "ai bordi" + video.addEventListener('click', (e) => e.stopPropagation()); + + return video; +} + +function createImageElement(srcOriginal, srcPreview) { + const img = document.createElement('img'); + img.src = srcPreview || srcOriginal || ''; + img.style.maxWidth = '100%'; + img.style.maxHeight = '100%'; + img.style.objectFit = 'contain'; + + // Progressive loading: preview → fullres + if (srcPreview && srcOriginal && srcPreview !== srcOriginal) { + const full = new Image(); + full.src = srcOriginal; + full.onload = () => { img.src = srcOriginal; }; + } + return img; +} + +// =============================== +// Helpers per URL assoluti +// =============================== + +function absUrl(path, name, type, cartella) { + // Se PATH_FULL è attivo, il backend ha già fornito un URL completo + if (window.PATH_FULL) { + return path; + } + + // Comportamento normale + return (typeof toAbsoluteUrl === 'function') + ? toAbsoluteUrl(path, name, type, cartella) + : path; +} + + +function mediaUrlsFromPhoto(photo) { + let original, preview; + + if (window.PATH_FULL) { + // Percorsi completi già pronti dal backend + original = photo?.path; + preview = photo?.thub2 || photo?.thub1 || photo?.path; + } else { + // Costruzione URL come prima + original = absUrl(photo?.path, photo?.user, "original", photo?.cartella); + + preview = absUrl( + photo?.thub2 || photo?.thub1 || photo?.path, + photo?.user, + "thumbs", + photo?.cartella + ); + } + + return { original, preview }; +} + + + +// =============================== +// PRELOAD ±N (solo immagini; per i video: poster/preview) +// =============================== +function preloadNeighbors(N = 3) { + const list = window.modalList || []; + const idx = window.modalIndex || 0; + + for (let offset = 1; offset <= N; offset++) { + const iPrev = idx - offset; + const iNext = idx + offset; + [iPrev, iNext].forEach(i => { + const p = list[i]; + if (!p) return; + const { original, preview } = mediaUrlsFromPhoto(p); + const isVideo = String(p?.mime_type || '').toLowerCase().startsWith('video/'); + const src = isVideo ? (preview || original) : original; + if (!src) return; + const img = new Image(); + img.src = src; + }); + } +} + +// =============================== +// Core: imposta contenuto modal +// =============================== +function setModalContent(photo, srcOriginal, srcPreview) { + const container = document.getElementById('modalMediaContainer'); + container.innerHTML = ''; + window.currentPhoto = photo; + + const isVideo = isProbablyVideo(photo, srcOriginal); + console.log('[openModal]', { isVideo, mime: photo?.mime_type, srcOriginal, srcPreview }); + + if (isVideo) { + const video = createVideoElement(srcOriginal, srcPreview, photo); + container.appendChild(video); + } else { + const img = createImageElement(srcOriginal, srcPreview); + container.appendChild(img); + } + + // Pulsante INFO (ℹ️) dentro il modal — toggle vero + const infoBtn = document.createElement('button'); + infoBtn.id = 'modalInfoBtn'; + infoBtn.className = 'modal-info-btn'; + infoBtn.type = 'button'; + infoBtn.setAttribute('aria-label', 'Dettagli'); + infoBtn.textContent = 'ℹ️'; + container.appendChild(infoBtn); + + infoBtn.addEventListener('click', (e) => { + e.stopPropagation(); // non far scattare navigazione + toggleInfo(window.currentPhoto); + }); +} + +// =============================== +// API base: open/close modal (mantiene sostituzione contenuto) +// =============================== +function openModal(srcOriginal, srcPreview, photo) { + // Chiudi sempre la strip prima di aprire + window.closeBottomSheet?.(); + + setModalContent(photo, srcOriginal, srcPreview); + modal.classList.add('open'); + modal.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; +} + +function closeModal() { + // Chiudi anche l'info se aperto + if (isInfoOpen()) closeInfo(); + + const v = document.querySelector('#modal video'); + if (v) { + try { v.pause(); } catch (_) {} + v.removeAttribute('src'); + while (v.firstChild) v.removeChild(v.firstChild); + try { v.load(); } catch (_) {} + } + const container = document.getElementById('modalMediaContainer'); + if (container) container.innerHTML = ''; + modal.classList.remove('open'); + modal.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + + // Nascondi frecce alla chiusura (così non "rimangono" visibili) + try { + modalPrev?.classList.add('hidden'); + modalNext?.classList.add('hidden'); + } catch {} +} + +// X: stopPropagation + chiudi +modalClose?.addEventListener('click', (e) => { e.stopPropagation(); closeModal(); }); + +// Backdrop: chiudi cliccando fuori +modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); + +// =============================== +// Navigazione: lista + indice + prev/next + click ai bordi + tastiera +// =============================== +function openAt(i) { + const list = window.modalList || []; + if (!list[i]) return; + window.modalIndex = i; + const photo = list[i]; + const { original, preview } = mediaUrlsFromPhoto(photo); + + // Se l'info è aperto, aggiorna i contenuti per la nuova foto + if (isInfoOpen()) { + openInfo(photo); + } + + openModal(original, preview, photo); // sostituisce contenuto + preloadNeighbors(3); + updateArrows(); +} + +window.openModalFromList = function(list, index) { + window.modalList = Array.isArray(list) ? list : []; + window.modalIndex = Math.max(0, Math.min(index || 0, window.modalList.length - 1)); + openAt(window.modalIndex); +}; + +function showPrev() { if (window.modalIndex > 0) openAt(window.modalIndex - 1); } +function showNext() { if (window.modalIndex < (window.modalList.length - 1)) openAt(window.modalIndex + 1); } + +// Tastiera +document.addEventListener('keydown', (e) => { + if (!modal.classList.contains('open')) return; + if (e.key === 'ArrowLeft') { e.preventDefault(); showPrev(); } + if (e.key === 'ArrowRight') { e.preventDefault(); showNext(); } +}); + +// Click ai bordi del modal: sinistra=prev, destra=next (ignora controlli) +modal.addEventListener('click', (e) => { + if (!modal.classList.contains('open')) return; + + // Ignora click sui controlli + if (e.target.closest('.modal-info-btn, .modal-close, .modal-nav-btn')) return; + + if (e.target === modal) return; // già gestito per chiusura + + const rect = modal.getBoundingClientRect(); + const x = e.clientX - rect.left; + const side = x / rect.width; + if (side < 0.25) showPrev(); + else if (side > 0.75) showNext(); +}); + +// Esporta API base (per compatibilità con codice esistente) +window.openModal = openModal; +window.closeModal = closeModal; + +// =============================== +// FRECCE DI NAVIGAZIONE < > +// =============================== +function updateArrows() { + if (!modalPrev || !modalNext) return; + const len = (window.modalList || []).length; + const i = window.modalIndex || 0; + + // Mostra frecce solo se ci sono almeno 2 elementi + const show = len > 1; + modalPrev.classList.toggle('hidden', !show); + modalNext.classList.toggle('hidden', !show); + + // Disabilita ai bordi (no wrap) + modalPrev.classList.toggle('disabled', i <= 0); + modalNext.classList.toggle('disabled', i >= len - 1); +} + +// Click sulle frecce: non propagare (evita conflitti col click sui bordi) +modalPrev?.addEventListener('click', (e) => { e.stopPropagation(); showPrev(); updateArrows(); }); +modalNext?.addEventListener('click', (e) => { e.stopPropagation(); showNext(); updateArrows(); }); diff --git a/public/photos/Common1/original/IMG_20220619_135541.jpg b/public/photos/Common1/original/IMG_20220619_135541.jpg new file mode 100644 index 0000000..908ce9b Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135541.jpg differ diff --git a/public/photos/Common1/original/IMG_20220619_135542.jpg b/public/photos/Common1/original/IMG_20220619_135542.jpg new file mode 100644 index 0000000..36fc176 Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135542.jpg differ diff --git a/public/photos/Common1/original/IMG_20220619_135543.jpg b/public/photos/Common1/original/IMG_20220619_135543.jpg new file mode 100644 index 0000000..de46c31 Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135543.jpg differ diff --git a/public/photos/Common1/original/IMG_20220619_135636.jpg b/public/photos/Common1/original/IMG_20220619_135636.jpg new file mode 100644 index 0000000..8351fba Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135636.jpg differ diff --git a/public/photos/Common1/original/IMG_20220619_135641.jpg b/public/photos/Common1/original/IMG_20220619_135641.jpg new file mode 100644 index 0000000..8c67906 Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135641.jpg differ diff --git a/public/photos/Common1/original/IMG_20220619_135643.jpg b/public/photos/Common1/original/IMG_20220619_135643.jpg new file mode 100644 index 0000000..b0a820a Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135643.jpg differ diff --git a/public/photos/Common1/original/IMG_20220619_135741.jpg b/public/photos/Common1/original/IMG_20220619_135741.jpg new file mode 100644 index 0000000..5dd4d77 Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135741.jpg differ diff --git a/public/photos/Common1/original/IMG_20220619_135744.jpg b/public/photos/Common1/original/IMG_20220619_135744.jpg new file mode 100644 index 0000000..11e6c42 Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135744.jpg differ diff --git a/public/photos/Common1/original/IMG_20220619_135745.jpg b/public/photos/Common1/original/IMG_20220619_135745.jpg new file mode 100644 index 0000000..f5cf90d Binary files /dev/null and b/public/photos/Common1/original/IMG_20220619_135745.jpg differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0092.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0092.JPG new file mode 100644 index 0000000..fb44c2e Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0092.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0099.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0099.JPG new file mode 100644 index 0000000..bbf6b8f Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0099.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0100.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0100.JPG new file mode 100644 index 0000000..cadc7d5 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0100.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0102.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0102.JPG new file mode 100644 index 0000000..4401f38 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0102.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0103.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0103.JPG new file mode 100644 index 0000000..78252b2 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0103.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0104.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0104.JPG new file mode 100644 index 0000000..7938c88 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0104.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0106.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0106.JPG new file mode 100644 index 0000000..6f255a3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0106.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0107.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0107.JPG new file mode 100644 index 0000000..721f942 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0107.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0108.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0108.JPG new file mode 100644 index 0000000..81656da Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0108.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0109.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0109.JPG new file mode 100644 index 0000000..d9cf826 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0109.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0110.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0110.JPG new file mode 100644 index 0000000..93cfa80 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0110.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0112.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0112.JPG new file mode 100644 index 0000000..897ae3d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0112.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0113.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0113.JPG new file mode 100644 index 0000000..8a5235b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0113.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0114.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0114.JPG new file mode 100644 index 0000000..1537b13 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0114.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0116.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0116.JPG new file mode 100644 index 0000000..6acf3c5 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0116.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0119.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0119.JPG new file mode 100644 index 0000000..0156bcb Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0119.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0120.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0120.JPG new file mode 100644 index 0000000..6ecad02 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0120.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0122.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0122.JPG new file mode 100644 index 0000000..b33b484 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0122.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0123.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0123.JPG new file mode 100644 index 0000000..c3088f0 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0123.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0124.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0124.JPG new file mode 100644 index 0000000..ff5c90c Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0124.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0125.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0125.JPG new file mode 100644 index 0000000..a741447 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0125.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0126.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0126.JPG new file mode 100644 index 0000000..71f70c1 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0126.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0133.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0133.JPG new file mode 100644 index 0000000..c8001b7 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0133.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0134.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0134.JPG new file mode 100644 index 0000000..79ff160 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0134.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0135.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0135.JPG new file mode 100644 index 0000000..ca95e34 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0135.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0136.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0136.JPG new file mode 100644 index 0000000..92f1d1f Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0136.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0137.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0137.JPG new file mode 100644 index 0000000..0b122c3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0137.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0138.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0138.JPG new file mode 100644 index 0000000..534ffe8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0138.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0139.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0139.JPG new file mode 100644 index 0000000..f47c0ff Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0139.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0140.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0140.JPG new file mode 100644 index 0000000..41b9b20 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0140.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0141.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0141.JPG new file mode 100644 index 0000000..d4c4226 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0141.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0143.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0143.JPG new file mode 100644 index 0000000..db5aa75 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0143.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0145.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0145.JPG new file mode 100644 index 0000000..3fb3078 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0145.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0146.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0146.JPG new file mode 100644 index 0000000..7cc88a8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0146.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0147.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0147.JPG new file mode 100644 index 0000000..e7bdd87 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0147.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0148.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0148.JPG new file mode 100644 index 0000000..c8994d3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0148.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0149.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0149.JPG new file mode 100644 index 0000000..ccc11c8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0149.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0150.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0150.JPG new file mode 100644 index 0000000..a662369 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0150.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0152.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0152.JPG new file mode 100644 index 0000000..1ee886d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0152.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0153.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0153.JPG new file mode 100644 index 0000000..777a4c0 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0153.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0154.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0154.JPG new file mode 100644 index 0000000..e5a14b3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0154.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0155.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0155.JPG new file mode 100644 index 0000000..f61fa2b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0155.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0156.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0156.JPG new file mode 100644 index 0000000..4790319 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0156.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0157.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0157.JPG new file mode 100644 index 0000000..47d96af Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0157.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0160.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0160.JPG new file mode 100644 index 0000000..6011b00 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0160.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0162.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0162.JPG new file mode 100644 index 0000000..3c16a00 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0162.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0163.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0163.JPG new file mode 100644 index 0000000..dcdbd55 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0163.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0164.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0164.JPG new file mode 100644 index 0000000..946059d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0164.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0165.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0165.JPG new file mode 100644 index 0000000..1e5cb4b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0165.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0166.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0166.JPG new file mode 100644 index 0000000..6e639c7 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0166.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0167.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0167.JPG new file mode 100644 index 0000000..4094874 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0167.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0170.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0170.JPG new file mode 100644 index 0000000..ebe7c55 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0170.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0171.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0171.JPG new file mode 100644 index 0000000..fa1e8e7 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0171.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0172.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0172.JPG new file mode 100644 index 0000000..08b8197 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0172.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0174.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0174.JPG new file mode 100644 index 0000000..ed83ac3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0174.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0175.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0175.JPG new file mode 100644 index 0000000..e1d4b51 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0175.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0176.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0176.JPG new file mode 100644 index 0000000..e8af76c Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0176.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0177.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0177.JPG new file mode 100644 index 0000000..f3ea4c8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0177.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0178.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0178.JPG new file mode 100644 index 0000000..0809fad Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0178.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0179.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0179.JPG new file mode 100644 index 0000000..f85e317 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0179.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0180.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0180.JPG new file mode 100644 index 0000000..d992ed6 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0180.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0182.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0182.JPG new file mode 100644 index 0000000..3e395d4 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0182.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0183.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0183.JPG new file mode 100644 index 0000000..9d8bc9c Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0183.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0185.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0185.JPG new file mode 100644 index 0000000..5ea5f9e Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0185.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0188.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0188.JPG new file mode 100644 index 0000000..2312168 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0188.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0190.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0190.JPG new file mode 100644 index 0000000..2dada06 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0190.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0193.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0193.JPG new file mode 100644 index 0000000..6398d18 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0193.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0203.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0203.JPG new file mode 100644 index 0000000..3b2699e Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0203.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0204.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0204.JPG new file mode 100644 index 0000000..744c2e7 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0204.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0206.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0206.JPG new file mode 100644 index 0000000..93f5a2d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0206.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0207.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0207.JPG new file mode 100644 index 0000000..6af3504 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0207.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0208.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0208.JPG new file mode 100644 index 0000000..82db55d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0208.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0209.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0209.JPG new file mode 100644 index 0000000..fee5f53 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0209.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0211.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0211.JPG new file mode 100644 index 0000000..9d35dd8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0211.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0212.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0212.JPG new file mode 100644 index 0000000..264d1e0 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0212.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0214.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0214.JPG new file mode 100644 index 0000000..6ac4dbe Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0214.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0215.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0215.JPG new file mode 100644 index 0000000..e894612 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0215.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0216.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0216.JPG new file mode 100644 index 0000000..3ac2b2b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0216.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0217.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0217.JPG new file mode 100644 index 0000000..5fe02c1 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0217.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0218.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0218.JPG new file mode 100644 index 0000000..4e2211a Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0218.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0223.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0223.JPG new file mode 100644 index 0000000..7a8b3df Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0223.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0227.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0227.JPG new file mode 100644 index 0000000..5dcf97b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0227.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0228.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0228.JPG new file mode 100644 index 0000000..8aac3ae Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0228.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0229.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0229.JPG new file mode 100644 index 0000000..139cc35 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0229.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0232.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0232.JPG new file mode 100644 index 0000000..725e489 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0232.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0233.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0233.JPG new file mode 100644 index 0000000..d8f588e Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0233.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0234.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0234.JPG new file mode 100644 index 0000000..71c1f0b Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0234.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0235.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0235.JPG new file mode 100644 index 0000000..caf3d16 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0235.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0237.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0237.JPG new file mode 100644 index 0000000..57e5683 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0237.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0241.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0241.JPG new file mode 100644 index 0000000..c8a254d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0241.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0242.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0242.JPG new file mode 100644 index 0000000..0c8414a Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0242.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0243.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0243.JPG new file mode 100644 index 0000000..a5749a6 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0243.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0244.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0244.JPG new file mode 100644 index 0000000..749e6ab Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0244.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0245.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0245.JPG new file mode 100644 index 0000000..b79ede8 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0245.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0246.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0246.JPG new file mode 100644 index 0000000..fe7d69a Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0246.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0247.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0247.JPG new file mode 100644 index 0000000..cb6f053 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0247.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0248.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0248.JPG new file mode 100644 index 0000000..4063c0d Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0248.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0249.JPG b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0249.JPG new file mode 100644 index 0000000..2e872f3 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/IMG_0249.JPG differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/VID_20260221_095917.mp4 b/public/photos/Fabio/original/2017Irlanda19-29ago/VID_20260221_095917.mp4 new file mode 100644 index 0000000..54b7938 Binary files /dev/null and b/public/photos/Fabio/original/2017Irlanda19-29ago/VID_20260221_095917.mp4 differ diff --git a/public/photos/Fabio/original/2017Irlanda19-29ago/t.js b/public/photos/Fabio/original/2017Irlanda19-29ago/t.js new file mode 100644 index 0000000..2a3d8e8 --- /dev/null +++ b/public/photos/Fabio/original/2017Irlanda19-29ago/t.js @@ -0,0 +1,9 @@ +const { exec } = require('child_process'); +const video = process.argv[2]; +const cmd = `exiftool -n -G1 -a -gps:all -quicktime:all -user:all "${video}"`; +console.log("Eseguo:", cmd); +exec(cmd, (err, stdout, stderr) => { + console.log("err:", err); + console.log("stderr:", stderr ? stderr.slice(0,1000) : ""); + console.log("stdout (head):", stdout ? stdout.slice(0,2000) : ""); +}); diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100423.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100423.jpg new file mode 100644 index 0000000..84e227b Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100423.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100428.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100428.jpg new file mode 100644 index 0000000..bebd335 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100428.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100449.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100449.jpg new file mode 100644 index 0000000..03cfd8e Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100449.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100532.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100532.jpg new file mode 100644 index 0000000..7f80413 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100532.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100557.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100557.jpg new file mode 100644 index 0000000..11c5fbf Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100557.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100637.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100637.jpg new file mode 100644 index 0000000..5a720a3 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100637.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100703.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100703.jpg new file mode 100644 index 0000000..17c8898 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100703.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100736.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100736.jpg new file mode 100644 index 0000000..6a01a80 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_100736.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104441.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104441.jpg new file mode 100644 index 0000000..b2dda4b Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104441.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104444.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104444.jpg new file mode 100644 index 0000000..bdeae55 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_104444.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140046.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140046.jpg new file mode 100644 index 0000000..1405bfc Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140046.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140118.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140118.jpg new file mode 100644 index 0000000..941d712 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140118.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140120.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140120.jpg new file mode 100644 index 0000000..5133b01 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140120.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140215.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140215.jpg new file mode 100644 index 0000000..d6c7fac Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140215.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140246.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140246.jpg new file mode 100644 index 0000000..a208376 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_140246.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173903.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173903.jpg new file mode 100644 index 0000000..904762d Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173903.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173923.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173923.jpg new file mode 100644 index 0000000..219bd81 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_173923.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180652.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180652.jpg new file mode 100644 index 0000000..3781e6c Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180652.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180710.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180710.jpg new file mode 100644 index 0000000..a63cfd3 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180710.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180723.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180723.jpg new file mode 100644 index 0000000..e84173d Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_180723.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181011.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181011.jpg new file mode 100644 index 0000000..21e2d24 Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181011.jpg differ diff --git a/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181034.jpg b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181034.jpg new file mode 100644 index 0000000..31e4f6b Binary files /dev/null and b/public/photos/Jessica/original/2021-Santarcangelo/IMG_20210602_181034.jpg differ diff --git a/public/photos/index.json b/public/photos/index.json new file mode 100644 index 0000000..39ae2f0 --- /dev/null +++ b/public/photos/index.json @@ -0,0 +1 @@ +{"a108438f4073dbd460157fff35ee00792c4267f7d242601ac9e61478ef850aee":{"id":"a108438f4073dbd460157fff35ee00792c4267f7d242601ac9e61478ef850aee","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0092.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0092_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0092_avg.jpg","gps":{"lat":44.17688333333333,"lng":11.956669444444444,"alt":38.888513513513516},"data":"2017:08:19 11:30:14","taken_at":"2017-08-19T11:30:14.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3492198,"mtimeMs":1772610987381.1497,"duration":null,"location":{"continent":"Europe","country":"Italy","region":"Emilia-Romagna","postcode":"47011","city":"Castrocaro Terme e Terra del Sole","county_code":"FC","address":"Via Plebino Battanini, 19","timezone":"Europe/Rome","time":"+01:00"}},"b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e":{"id":"b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0099.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0099_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0099_avg.jpg","gps":{"lat":45.66611944444444,"lng":9.701108333333332,"alt":229.64888888888888},"data":"2017:08:23 09:49:00","taken_at":"2017-08-23T09:49:00.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3758888,"mtimeMs":1772610987384.483,"duration":null,"location":{"continent":"Europe","country":"Italy","region":"Lombardy","postcode":"24050","city":"Orio al Serio","county_code":"BG","address":"WorkEat","timezone":"Europe/Rome","time":"+01:00"}},"ef552a509446caaa2faf1fc3d285518bc243a4e087108282f662d9818e9063d9":{"id":"ef552a509446caaa2faf1fc3d285518bc243a4e087108282f662d9818e9063d9","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0100.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0100_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0100_avg.jpg","gps":{"lat":45.66613888888889,"lng":9.70108611111111,"alt":227.6668260038241},"data":"2017:08:23 09:49:01","taken_at":"2017-08-23T09:49:01.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4131423,"mtimeMs":1772610987387.8162,"duration":null,"location":{"continent":"Europe","country":"Italy","region":"Lombardy","postcode":"24050","city":"Orio al Serio","county_code":"BG","address":"WorkEat","timezone":"Europe/Rome","time":"+01:00"}},"550d4f611da31ff221adb97220eae756cb74945e8b6d228ca5cbfc31216ac2e5":{"id":"550d4f611da31ff221adb97220eae756cb74945e8b6d228ca5cbfc31216ac2e5","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0102.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0102_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0102_avg.jpg","gps":{"lat":53.35111666666667,"lng":-6.251255555555556,"alt":38.25333333333333},"data":"2017:08:23 12:44:30","taken_at":"2017-08-23T12:44:30.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2534029,"mtimeMs":1772610987387.8162,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D01 K298","city":"Dublin","county_code":"D","address":"Molloy’s","timezone":"Europe/Dublin","time":"+01:00"}},"09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b":{"id":"09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0103.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0103_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0103_avg.jpg","gps":{"lat":53.344925,"lng":-6.256825,"alt":6.371601208459214},"data":"2017:08:23 13:08:20","taken_at":"2017-08-23T13:08:20.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3984165,"mtimeMs":1772610987391.1494,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 WN62","city":"Dublin","county_code":"D","address":"17 Botany Bay","timezone":"Europe/Dublin","time":"+01:00"}},"80b68da7f4153b6f32d9161a9593e10611b8977288e755c66f12a62779a690e2":{"id":"80b68da7f4153b6f32d9161a9593e10611b8977288e755c66f12a62779a690e2","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0104.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0104_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0104_avg.jpg","gps":{"lat":53.34393055555556,"lng":-6.248975,"alt":10.23343653250774},"data":"2017:08:23 13:27:57","taken_at":"2017-08-23T13:27:57.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1918664,"mtimeMs":1772610987394.483,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 Y729","city":"Dublin","county_code":"D","address":"O'Donovan","timezone":"Europe/Dublin","time":"+01:00"}},"dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7":{"id":"dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0106.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0106_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0106_avg.jpg","gps":{"lat":53.343941666666666,"lng":-6.249247222222222,"alt":7.379692832764505},"data":"2017:08:23 13:48:41","taken_at":"2017-08-23T13:48:41.000Z","mime_type":"image/jpeg","width":5326,"height":3950,"size_bytes":4338120,"mtimeMs":1772610987397.8162,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 Y729","city":"Dublin","county_code":"D","address":"The Lombard Townhouse","timezone":"Europe/Dublin","time":"+01:00"}},"751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0":{"id":"751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0107.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0107_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0107_avg.jpg","gps":{"lat":53.344861111111115,"lng":-6.252305555555555,"alt":7.89636462289745},"data":"2017:08:23 13:59:45","taken_at":"2017-08-23T13:59:45.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3159006,"mtimeMs":1772610987401.1494,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 VX62","city":"Dublin","county_code":"D","address":"O’Neill’s of Pearse Street","timezone":"Europe/Dublin","time":"+01:00"}},"73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937":{"id":"73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0108.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0108_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0108_avg.jpg","gps":{"lat":53.344791666666666,"lng":-6.252316666666666,"alt":7.7956509618065235},"data":"2017:08:23 13:59:47","taken_at":"2017-08-23T13:59:47.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3338305,"mtimeMs":1772610987404.4827,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 VX62","city":"Dublin","county_code":"D","address":"O’Neill’s of Pearse Street","timezone":"Europe/Dublin","time":"+01:00"}},"f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8":{"id":"f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0109.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0109_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0109_avg.jpg","gps":{"lat":53.34500555555556,"lng":-6.253313888888889,"alt":8.374616171954964},"data":"2017:08:23 14:03:17","taken_at":"2017-08-23T14:03:17.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2815021,"mtimeMs":1772610987407.816,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 VX62","city":"Dublin","county_code":"D","address":"29 Pearse Street","timezone":"Europe/Dublin","time":"+01:00"}},"90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe":{"id":"90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0110.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0110_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0110_avg.jpg","gps":{"lat":53.345688888888894,"lng":-6.261755555555555,"alt":11.630410654827969},"data":"2017:08:23 14:17:34","taken_at":"2017-08-23T14:17:34.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3207260,"mtimeMs":1772610987411.1492,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 N159","city":"Dublin","county_code":"D","address":"The Oliver St John Gogarty","timezone":"Europe/Dublin","time":"+01:00"}},"4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935":{"id":"4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0112.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0112_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0112_avg.jpg","gps":{"lat":53.34568055555556,"lng":-6.261855555555556,"alt":11.982026143790849},"data":"2017:08:23 14:17:53","taken_at":"2017-08-23T14:17:53.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2674913,"mtimeMs":1772610987414.4827,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 ET66","city":"Dublin","county_code":"D","address":"23 Temple Bar","timezone":"Europe/Dublin","time":"+01:00"}},"1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f":{"id":"1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0113.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0113_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0113_avg.jpg","gps":{"lat":53.34517777777778,"lng":-6.265841666666667,"alt":15.984316185696361},"data":"2017:08:23 14:36:10","taken_at":"2017-08-23T14:36:10.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2788605,"mtimeMs":1772610987417.816,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 PW83","city":"Dublin","county_code":"D","address":"ESSEX STREET EAST (SE)","timezone":"Europe/Dublin","time":"+01:00"}},"14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2":{"id":"14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0114.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0114_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0114_avg.jpg","gps":{"lat":53.339866666666666,"lng":-6.269975,"alt":12.443296305864118},"data":"2017:08:23 16:13:54","taken_at":"2017-08-23T16:13:54.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3447883,"mtimeMs":1772610987421.1492,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D07 CF98","city":"Dublin","county_code":"D","address":"St Patrick's Park Tea Gardens","timezone":"Europe/Dublin","time":"+01:00"}},"7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41":{"id":"7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0116.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0116_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0116_avg.jpg","gps":{"lat":53.34003333333334,"lng":-6.270691666666667,"alt":11.41955017301038},"data":"2017:08:23 16:18:23","taken_at":"2017-08-23T16:18:23.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3513469,"mtimeMs":1772610987424.4824,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D07 CF98","city":"Dublin","county_code":"D","address":"Liberty Bell","timezone":"Europe/Dublin","time":"+01:00"}},"4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c":{"id":"4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0119.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0119_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0119_avg.jpg","gps":{"lat":53.34229722222222,"lng":-6.283711111111111,"alt":20.334302325581394},"data":"2017:08:23 17:23:54","taken_at":"2017-08-23T17:23:54.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2353262,"mtimeMs":1772610987427.8157,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 VF83","city":"Dublin","county_code":"D","address":"Saint James' Gate","timezone":"Europe/Dublin","time":"+01:00"}},"490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750":{"id":"490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0120.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0120_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0120_avg.jpg","gps":{"lat":53.34181944444445,"lng":-6.28665,"alt":23.09464285714286},"data":"2017:08:23 17:41:46","taken_at":"2017-08-23T17:41:46.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2912549,"mtimeMs":1772610987431.149,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 YV8A","city":"Dublin","county_code":"D","address":"The Store","timezone":"Europe/Dublin","time":"+01:00"}},"1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be":{"id":"1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0122.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0122_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0122_avg.jpg","gps":{"lat":53.34172777777778,"lng":-6.286511111111111,"alt":23.021686746987953},"data":"2017:08:23 17:42:09","taken_at":"2017-08-23T17:42:09.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2351855,"mtimeMs":1772610987431.149,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 YV8A","city":"Dublin","county_code":"D","address":"1837","timezone":"Europe/Dublin","time":"+01:00"}},"486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3":{"id":"486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0123.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0123_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0123_avg.jpg","gps":{"lat":53.34194166666667,"lng":-6.286716666666667,"alt":23.662634408602152},"data":"2017:08:23 18:27:09","taken_at":"2017-08-23T18:27:09.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1869483,"mtimeMs":1772610987434.4822,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 YV8A","city":"Dublin","county_code":"D","address":"Roasthouse","timezone":"Europe/Dublin","time":"+01:00"}},"69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a":{"id":"69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0124.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0124_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0124_avg.jpg","gps":{"lat":53.341975000000005,"lng":-6.286752777777778,"alt":23.81895093062606},"data":"2017:08:23 18:27:22","taken_at":"2017-08-23T18:27:22.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1800632,"mtimeMs":1772610987434.4822,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 YV8A","city":"Dublin","county_code":"D","address":"Roasthouse","timezone":"Europe/Dublin","time":"+01:00"}},"1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2":{"id":"1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0125.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0125_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0125_avg.jpg","gps":{"lat":53.34193055555556,"lng":-6.285958333333333,"alt":22.37097064649739},"data":"2017:08:23 18:53:11","taken_at":"2017-08-23T18:53:11.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1643315,"mtimeMs":1772610987437.8157,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 YV8A","city":"Dublin","county_code":"D","address":"Gravity Bar","timezone":"Europe/Dublin","time":"+01:00"}},"7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a":{"id":"7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0126.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0126_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0126_avg.jpg","gps":{"lat":53.341911111111116,"lng":-6.286,"alt":22.44620811287478},"data":"2017:08:23 18:53:23","taken_at":"2017-08-23T18:53:23.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1450013,"mtimeMs":1772610987437.8157,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 YV8A","city":"Dublin","county_code":"D","address":"Gravity Bar","timezone":"Europe/Dublin","time":"+01:00"}},"35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac":{"id":"35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0133.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0133_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0133_avg.jpg","gps":{"lat":53.34184166666667,"lng":-6.286008333333333,"alt":22.69865067466267},"data":"2017:08:23 18:53:53","taken_at":"2017-08-23T18:53:53.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1606095,"mtimeMs":1772610987441.149,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 YV8A","city":"Dublin","county_code":"D","address":"BELVIEW (SC)","timezone":"Europe/Dublin","time":"+01:00"}},"48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274":{"id":"48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0134.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0134_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0134_avg.jpg","gps":{"lat":53.343222222222224,"lng":-6.281552777777778,"alt":23},"data":"2017:08:23 20:08:55","taken_at":"2017-08-23T20:08:55.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1839090,"mtimeMs":1772610987441.149,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D08 VF83","city":"Dublin","county_code":"D","address":"Arthur’s","timezone":"Europe/Dublin","time":"+01:00"}},"10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134":{"id":"10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0135.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0135_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0135_avg.jpg","gps":{"lat":53.34328055555556,"lng":-6.281516666666667,"alt":23},"data":"2017:08:23 20:26:16","taken_at":"2017-08-23T20:26:16.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2013689,"mtimeMs":1772610987444.4822,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Dublin","postcode":"D16","city":"Dublin","county_code":"DN","address":"Morelli's","timezone":"Europe/Dublin","time":"+01:00"}},"c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969":{"id":"c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0136.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0136_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0136_avg.jpg","gps":null,"data":null,"taken_at":null,"mime_type":"image/jpeg","width":3724,"height":2096,"size_bytes":1557955,"mtimeMs":1772610987444.4822,"duration":null,"location":null},"1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90":{"id":"1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0137.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0137_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0137_avg.jpg","gps":{"lat":53.34129444444444,"lng":-6.2516,"alt":22.956375838926174},"data":"2017:08:24 08:36:55","taken_at":"2017-08-24T08:36:55.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2667599,"mtimeMs":1772610987447.8154,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 TD34","city":"Dublin","county_code":"D","address":"Hansel & Gretel","timezone":"Europe/Dublin","time":"+01:00"}},"bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0":{"id":"bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0138.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0138_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0138_avg.jpg","gps":{"lat":53.34135555555556,"lng":-6.251561111111111,"alt":22.956375838926174},"data":"2017:08:24 08:36:59","taken_at":"2017-08-24T08:36:59.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2698118,"mtimeMs":1772610987451.1487,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Leinster","postcode":"D02 TD34","city":"Dublin","county_code":"D","address":"Dublin (Clare Street)","timezone":"Europe/Dublin","time":"+01:00"}},"7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f":{"id":"7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0139.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0139_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0139_avg.jpg","gps":{"lat":53.34143888888889,"lng":-6.251458333333333,"alt":22.981879954699888},"data":"2017:08:24 08:37:18","taken_at":"2017-08-24T08:37:18.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2656457,"mtimeMs":1772610987454.482,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Dublin","postcode":"D02","city":"Dublin","county_code":"DN","address":"20 Clare Street","timezone":"Europe/Dublin","time":"+01:00"}},"29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b":{"id":"29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0140.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0140_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0140_avg.jpg","gps":{"lat":53.010505555555554,"lng":-6.326797222222222,"alt":129.97678275290215},"data":"2017:08:24 14:09:42","taken_at":"2017-08-24T14:09:42.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4654237,"mtimeMs":1772610987457.8154,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Wicklow","postcode":"A98 X9C1","city":"The Municipal District of Wicklow","county_code":"WW","address":"Glendalough Cathedral","timezone":"Europe/Dublin","time":"+01:00"}},"ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839":{"id":"ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0141.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0141_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0141_avg.jpg","gps":{"lat":53.00955277777778,"lng":-6.324516666666667,"alt":138.50180505415162},"data":"2017:08:24 14:22:33","taken_at":"2017-08-24T14:22:33.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3760564,"mtimeMs":1772610987461.1487,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Wicklow","postcode":"A98 X9C1","city":"The Municipal District of Wicklow","county_code":"WW","address":"Glendasan River","timezone":"Europe/Dublin","time":"+01:00"}},"c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6":{"id":"c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0143.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0143_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0143_avg.jpg","gps":{"lat":52.650172222222224,"lng":-7.24905,"alt":71.83597883597884},"data":"2017:08:24 17:15:28","taken_at":"2017-08-24T17:15:28.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3627847,"mtimeMs":1772610987467.8152,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Kilkenny City","postcode":"R95 P77E","city":"Kilkenny","county_code":"KK","address":"Kilkenny Castle","timezone":"Europe/Dublin","time":"+01:00"}},"f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f":{"id":"f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0145.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0145_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0145_avg.jpg","gps":{"lat":52.519283333333334,"lng":-7.886636111111111,"alt":117.002457002457},"data":"2017:08:24 20:18:42","taken_at":"2017-08-24T20:18:42.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1900418,"mtimeMs":1772610987471.1484,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 PN72","city":"Cashel","county_code":"TA","address":"O Neills Restaurant","timezone":"Europe/Dublin","time":"+01:00"}},"5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e":{"id":"5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0146.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0146_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0146_avg.jpg","gps":{"lat":52.519238888888886,"lng":-7.886694444444444,"alt":118.54961832061069},"data":"2017:08:24 20:18:49","taken_at":"2017-08-24T20:18:49.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1944490,"mtimeMs":1772610987471.1484,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 PN72","city":"Cashel","county_code":"TA","address":"O Neills Restaurant","timezone":"Europe/Dublin","time":"+01:00"}},"dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c":{"id":"dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0147.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0147_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0147_avg.jpg","gps":{"lat":52.519283333333334,"lng":-7.886638888888888,"alt":117},"data":"2017:08:24 20:28:38","taken_at":"2017-08-24T20:28:38.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2020943,"mtimeMs":1772610987474.4817,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 PN72","city":"Cashel","county_code":"TA","address":"O Neills Restaurant","timezone":"Europe/Dublin","time":"+01:00"}},"8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8":{"id":"8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0148.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0148_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0148_avg.jpg","gps":{"lat":52.519730555555554,"lng":-7.888702777777778,"alt":111.23896103896104},"data":"2017:08:25 10:14:14","taken_at":"2017-08-25T10:14:14.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2632829,"mtimeMs":1772610987477.8152,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 PN72","city":"Cashel","county_code":"TA","address":"Castle Ivy Lodge","timezone":"Europe/Dublin","time":"+01:00"}},"46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc":{"id":"46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0149.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0149_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0149_avg.jpg","gps":{"lat":52.519733333333335,"lng":-7.889491666666666,"alt":117.0272373540856},"data":"2017:08:25 10:15:59","taken_at":"2017-08-25T10:15:59.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3300375,"mtimeMs":1772610987477.8152,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"The Rock Of Cashel","timezone":"Europe/Dublin","time":"+01:00"}},"999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e":{"id":"999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0150.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0150_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0150_avg.jpg","gps":{"lat":52.519730555555554,"lng":-7.890758333333332,"alt":135.53086419753086},"data":"2017:08:25 10:22:34","taken_at":"2017-08-25T10:22:34.000Z","mime_type":"image/jpeg","width":5532,"height":3898,"size_bytes":4461504,"mtimeMs":1772610987484.4817,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"Rock of Cashel","timezone":"Europe/Dublin","time":"+01:00"}},"7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436":{"id":"7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0152.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0152_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0152_avg.jpg","gps":{"lat":52.52006111111111,"lng":-7.8907,"alt":133.4918699186992},"data":"2017:08:25 10:24:04","taken_at":"2017-08-25T10:24:04.000Z","mime_type":"image/jpeg","width":5232,"height":3872,"size_bytes":4964535,"mtimeMs":1772610987487.815,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"The Cathedral","timezone":"Europe/Dublin","time":"+01:00"}},"4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69":{"id":"4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0153.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0153_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0153_avg.jpg","gps":{"lat":52.52001111111111,"lng":-7.8906777777777775,"alt":133.92578125},"data":"2017:08:25 10:24:33","taken_at":"2017-08-25T10:24:33.000Z","mime_type":"image/jpeg","width":5300,"height":3908,"size_bytes":4837273,"mtimeMs":1772610987494.4814,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"Rock of Cashel","timezone":"Europe/Dublin","time":"+01:00"}},"d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4":{"id":"d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0154.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0154_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0154_avg.jpg","gps":{"lat":52.520066666666665,"lng":-7.8907083333333325,"alt":133.5343137254902},"data":"2017:08:25 10:25:11","taken_at":"2017-08-25T10:25:11.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1697485,"mtimeMs":1772610987494.4814,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"The Cathedral","timezone":"Europe/Dublin","time":"+01:00"}},"49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600":{"id":"49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0155.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0155_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0155_avg.jpg","gps":{"lat":52.52003611111111,"lng":-7.8905666666666665,"alt":134.34188034188034},"data":"2017:08:25 10:25:25","taken_at":"2017-08-25T10:25:25.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1680963,"mtimeMs":1772610987494.4814,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"The Cathedral","timezone":"Europe/Dublin","time":"+01:00"}},"a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7":{"id":"a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0156.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0156_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0156_avg.jpg","gps":{"lat":52.519930555555554,"lng":-7.8905361111111105,"alt":133.32001452960407},"data":"2017:08:25 10:26:11","taken_at":"2017-08-25T10:26:11.000Z","mime_type":"image/jpeg","width":3024,"height":4032,"size_bytes":1502040,"mtimeMs":1772610987497.815,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"The Cathedral","timezone":"Europe/Dublin","time":"+01:00"}},"fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6":{"id":"fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0157.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0157_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0157_avg.jpg","gps":{"lat":52.519755555555555,"lng":-7.8903888888888885,"alt":130.2235469448584},"data":"2017:08:25 10:27:23","taken_at":"2017-08-25T10:27:23.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1520788,"mtimeMs":1772610987497.815,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"Cormac's Chapel","timezone":"Europe/Dublin","time":"+01:00"}},"c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93":{"id":"c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0160.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0160_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0160_avg.jpg","gps":{"lat":52.520469444444444,"lng":-7.890605555555555,"alt":129.35097493036213},"data":"2017:08:25 10:39:31","taken_at":"2017-08-25T10:39:31.000Z","mime_type":"image/jpeg","width":8178,"height":3754,"size_bytes":4954269,"mtimeMs":1772610987504.4814,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Cahir — Cashel","postcode":"E25 R897","city":"Cashel","county_code":"TA","address":"Rock of Cashel","timezone":"Europe/Dublin","time":"+01:00"}},"7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d":{"id":"7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0162.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0162_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0162_avg.jpg","gps":{"lat":51.897780555555556,"lng":-8.474155555555555,"alt":9.365076057511981},"data":"2017:08:25 13:01:46","taken_at":"2017-08-25T13:01:46.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2747267,"mtimeMs":1772610987507.8147,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Munster","postcode":"T12 NC8Y","city":"Cork","county_code":"CO","address":"Mr. Bells","timezone":"Europe/Dublin","time":"+01:00"}},"23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f":{"id":"23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0163.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0163_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0163_avg.jpg","gps":{"lat":51.89773888888889,"lng":-8.47423888888889,"alt":9.51040525739321},"data":"2017:08:25 13:02:29","taken_at":"2017-08-25T13:02:29.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2808395,"mtimeMs":1772610987507.8147,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Munster","postcode":"T12 H244","city":"Cork","county_code":"CO","address":"English Market","timezone":"Europe/Dublin","time":"+01:00"}},"0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843":{"id":"0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0164.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0164_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0164_avg.jpg","gps":{"lat":51.897780555555556,"lng":-8.474155555555555,"alt":8.269830508474577},"data":"2017:08:25 13:02:59","taken_at":"2017-08-25T13:02:59.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3137643,"mtimeMs":1772610987511.148,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Munster","postcode":"T12 NC8Y","city":"Cork","county_code":"CO","address":"Mr. Bells","timezone":"Europe/Dublin","time":"+01:00"}},"64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f":{"id":"64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0165.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0165_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0165_avg.jpg","gps":{"lat":51.89773888888889,"lng":-8.474166666666667,"alt":9.730654761904763},"data":"2017:08:25 13:25:17","taken_at":"2017-08-25T13:25:17.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2929880,"mtimeMs":1772610987514.4812,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Munster","postcode":"T12 H244","city":"Cork","county_code":"CO","address":"English Market","timezone":"Europe/Dublin","time":"+01:00"}},"8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39":{"id":"8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0166.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0166_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0166_avg.jpg","gps":{"lat":51.89773611111111,"lng":-8.47423611111111,"alt":9.621908127208481},"data":"2017:08:25 13:25:24","taken_at":"2017-08-25T13:25:24.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2498451,"mtimeMs":1772610987517.8147,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Munster","postcode":"T12 H244","city":"Cork","county_code":"CO","address":"English Market","timezone":"Europe/Dublin","time":"+01:00"}},"ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8":{"id":"ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0167.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0167_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0167_avg.jpg","gps":{"lat":51.894675,"lng":-8.479277777777778,"alt":7.381074168797954},"data":"2017:08:25 13:55:48","taken_at":"2017-08-25T13:55:48.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3020402,"mtimeMs":1772610987521.148,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Cork","postcode":"T23","city":"Cork","county_code":"CO","address":"Bishop Street","timezone":"Europe/Dublin","time":"+01:00"}},"a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a":{"id":"a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0170.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0170_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0170_avg.jpg","gps":{"lat":51.899947222222224,"lng":-8.469758333333333,"alt":14.086469175340273},"data":"2017:08:25 14:26:42","taken_at":"2017-08-25T14:26:42.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2161605,"mtimeMs":1772610987521.148,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Munster","postcode":"T12 ERW9","city":"Cork","county_code":"CO","address":"Merchant's Quay Shopping Centre","timezone":"Europe/Dublin","time":"+01:00"}},"7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a":{"id":"7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0171.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0171_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0171_avg.jpg","gps":{"lat":52.05671666666667,"lng":-9.939161111111112,"alt":23.29501385041551},"data":"2017:08:25 18:49:11","taken_at":"2017-08-25T18:49:11.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2913137,"mtimeMs":1772610987524.4812,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V93 H5W9","city":"Glenbeigh","county_code":"KY","address":"Ashes Bar","timezone":"Europe/Dublin","time":"+01:00"}},"9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3":{"id":"9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0172.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0172_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0172_avg.jpg","gps":{"lat":52.05670833333333,"lng":-9.939161111111112,"alt":23.29501385041551},"data":"2017:08:25 18:49:26","taken_at":"2017-08-25T18:49:26.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3437757,"mtimeMs":1772610987527.8145,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V93 H5W9","city":"Glenbeigh","county_code":"KY","address":"Ashes Bar","timezone":"Europe/Dublin","time":"+01:00"}},"df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a":{"id":"df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0174.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0174_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0174_avg.jpg","gps":null,"data":null,"taken_at":null,"mime_type":"image/jpeg","width":3724,"height":2096,"size_bytes":1668522,"mtimeMs":1772610987531.1477,"duration":null,"location":null},"f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6":{"id":"f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0175.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0175_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0175_avg.jpg","gps":null,"data":null,"taken_at":null,"mime_type":"image/jpeg","width":3724,"height":2096,"size_bytes":1781263,"mtimeMs":1772610987534.481,"duration":null,"location":null},"68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56":{"id":"68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0176.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0176_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0176_avg.jpg","gps":{"lat":52.05294444444444,"lng":-9.941038888888889,"alt":50.20450885668277},"data":"2017:08:25 21:12:38","taken_at":"2017-08-25T21:12:38.000Z","mime_type":"image/jpeg","width":3763,"height":1657,"size_bytes":973684,"mtimeMs":1772610987534.481,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V93 H5W9","city":"Glenbeigh","county_code":"KY","address":"Castle View House","timezone":"Europe/Dublin","time":"+01:00"}},"6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4":{"id":"6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0177.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0177_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0177_avg.jpg","gps":null,"data":null,"taken_at":null,"mime_type":"image/jpeg","width":3724,"height":2096,"size_bytes":1311890,"mtimeMs":1772610987534.481,"duration":null,"location":null},"dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66":{"id":"dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0178.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0178_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0178_avg.jpg","gps":{"lat":52.05667777777777,"lng":-9.938977777777778,"alt":31.517301038062282},"data":"2017:08:26 08:01:20","taken_at":"2017-08-26T08:01:20.000Z","mime_type":"image/jpeg","width":7506,"height":3870,"size_bytes":4612510,"mtimeMs":1772610987541.1477,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V93 H5W9","city":"Glenbeigh","county_code":"KY","address":"Ashes Bar","timezone":"Europe/Dublin","time":"+01:00"}},"4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf":{"id":"4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0179.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0179_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0179_avg.jpg","gps":{"lat":52.056602777777776,"lng":-9.939122222222222,"alt":32.31821086261981},"data":"2017:08:26 08:08:53","taken_at":"2017-08-26T08:08:53.000Z","mime_type":"image/jpeg","width":5624,"height":3958,"size_bytes":4484747,"mtimeMs":1772610987544.481,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V93 H5W9","city":"Glenbeigh","county_code":"KY","address":"Ashes Bar","timezone":"Europe/Dublin","time":"+01:00"}},"4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc":{"id":"4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0180.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0180_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0180_avg.jpg","gps":{"lat":51.85628888888889,"lng":-10.367191666666667,"alt":259.4727272727273},"data":"2017:08:26 10:24:15","taken_at":"2017-08-26T10:24:15.000Z","mime_type":"image/jpeg","width":8968,"height":1560,"size_bytes":2873334,"mtimeMs":1772610987547.8142,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V23 PK58","city":"Kenmare Municipal District","county_code":"KY","address":"Cúm an Easpaig","timezone":"Europe/Dublin","time":"+01:00"}},"0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378":{"id":"0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0182.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0182_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0182_avg.jpg","gps":{"lat":51.837830555555556,"lng":-9.898847222222221,"alt":-1.232899022801303},"data":"2017:08:26 12:05:14","taken_at":"2017-08-26T12:05:14.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2168733,"mtimeMs":1772610987551.1475,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V93 C940","city":"Sneem","county_code":"KY","address":"John Egan","timezone":"Europe/Dublin","time":"+01:00"}},"4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836":{"id":"4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0183.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0183_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0183_avg.jpg","gps":{"lat":51.83782222222222,"lng":-9.89883888888889,"alt":-5.1377802077638055},"data":"2017:08:26 12:05:16","taken_at":"2017-08-26T12:05:16.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1891230,"mtimeMs":1772610987551.1475,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V93 C940","city":"Sneem","county_code":"KY","address":"John Egan","timezone":"Europe/Dublin","time":"+01:00"}},"76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984":{"id":"76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0185.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0185_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0185_avg.jpg","gps":{"lat":51.83785555555556,"lng":-9.898961111111111,"alt":13.99988174077578},"data":"2017:08:26 12:08:23","taken_at":"2017-08-26T12:08:23.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1766923,"mtimeMs":1772610987554.4807,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kenmare Municipal District","postcode":"V93 C940","city":"Sneem","county_code":"KY","address":"John Egan","timezone":"Europe/Dublin","time":"+01:00"}},"065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd":{"id":"065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0188.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0188_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0188_avg.jpg","gps":{"lat":52.96539722222222,"lng":-9.434244444444445,"alt":159.0921228304406},"data":"2017:08:26 17:13:40","taken_at":"2017-08-26T17:13:40.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2935053,"mtimeMs":1772610987554.4807,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","city":"West Clare Municipal District","county_code":"CE","address":"Grá & co.","timezone":"Europe/Dublin","time":"+01:00"}},"e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed":{"id":"e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0190.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0190_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0190_avg.jpg","gps":{"lat":52.965272222222225,"lng":-9.434625,"alt":165.5382585751979},"data":"2017:08:26 17:15:49","taken_at":"2017-08-26T17:15:49.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1852261,"mtimeMs":1772610987557.8142,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","county_code":"CE","address":"Grá & co.","timezone":"Europe/Dublin","time":"+01:00"}},"c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f":{"id":"c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0193.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0193_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0193_avg.jpg","gps":{"lat":52.965294444444446,"lng":-9.434625,"alt":167.75757575757575},"data":"2017:08:26 17:15:54","taken_at":"2017-08-26T17:15:54.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1936144,"mtimeMs":1772610987561.1475,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","county_code":"CE","address":"Grá & co.","timezone":"Europe/Dublin","time":"+01:00"}},"77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd":{"id":"77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0203.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0203_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0203_avg.jpg","gps":{"lat":52.96421388888889,"lng":-9.437794444444444,"alt":174.47921225382933},"data":"2017:08:26 17:23:03","taken_at":"2017-08-26T17:23:03.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1843400,"mtimeMs":1772610987561.1475,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","county_code":"CE","address":"Grá & co.","timezone":"Europe/Dublin","time":"+01:00"}},"be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7":{"id":"be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0204.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0204_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0204_avg.jpg","gps":{"lat":52.96882222222222,"lng":-9.430027777777777,"alt":149.95366795366795},"data":"2017:08:26 17:40:27","taken_at":"2017-08-26T17:40:27.000Z","mime_type":"image/jpeg","width":4658,"height":3974,"size_bytes":4221357,"mtimeMs":1772610987564.4807,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","city":"West Clare Municipal District","county_code":"CE","address":"The Cliffs of Moher","timezone":"Europe/Dublin","time":"+01:00"}},"6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7":{"id":"6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0206.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0206_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0206_avg.jpg","gps":{"lat":52.96886111111111,"lng":-9.429549999999999,"alt":162.0505617977528},"data":"2017:08:26 17:41:05","taken_at":"2017-08-26T17:41:05.000Z","mime_type":"image/jpeg","width":7305,"height":2311,"size_bytes":4179034,"mtimeMs":1772610987571.1472,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","city":"West Clare Municipal District","county_code":"CE","address":"The Cliffs of Moher","timezone":"Europe/Dublin","time":"+01:00"}},"ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db":{"id":"ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0207.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0207_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0207_avg.jpg","gps":{"lat":52.96893611111111,"lng":-9.429127777777778,"alt":158.71615720524017},"data":"2017:08:26 17:42:55","taken_at":"2017-08-26T17:42:55.000Z","mime_type":"image/jpeg","width":1680,"height":4030,"size_bytes":1750381,"mtimeMs":1772610987574.4805,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","city":"West Clare Municipal District","county_code":"CE","address":"The Cliffs of Moher","timezone":"Europe/Dublin","time":"+01:00"}},"3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf":{"id":"3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0208.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0208_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0208_avg.jpg","gps":{"lat":52.96890277777778,"lng":-9.429177777777777,"alt":159.49257425742573},"data":"2017:08:26 17:43:05","taken_at":"2017-08-26T17:43:05.000Z","mime_type":"image/jpeg","width":4427,"height":2389,"size_bytes":2046548,"mtimeMs":1772610987574.4805,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","city":"West Clare Municipal District","county_code":"CE","address":"The Cliffs of Moher","timezone":"Europe/Dublin","time":"+01:00"}},"3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722":{"id":"3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0209.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0209_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0209_avg.jpg","gps":{"lat":52.969455555555555,"lng":-9.428788888888889,"alt":164.52023121387282},"data":"2017:08:26 17:45:03","taken_at":"2017-08-26T17:45:03.000Z","mime_type":"image/jpeg","width":6438,"height":3900,"size_bytes":4328104,"mtimeMs":1772610987581.1472,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 KN9T","city":"West Clare Municipal District","county_code":"CE","address":"The Cliffs of Moher","timezone":"Europe/Dublin","time":"+01:00"}},"50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027":{"id":"50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0211.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0211_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0211_avg.jpg","gps":{"lat":53.008922222222225,"lng":-9.390380555555556,"alt":29.118190212373037},"data":"2017:08:26 20:38:14","taken_at":"2017-08-26T20:38:14.000Z","mime_type":"image/jpeg","width":3946,"height":2960,"size_bytes":703594,"mtimeMs":1772610987581.1472,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 FY67","city":"West Clare Municipal District","county_code":"CE","address":"Aille River","timezone":"Europe/Dublin","time":"+01:00"}},"df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e":{"id":"df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0212.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0212_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0212_avg.jpg","gps":{"lat":53.00888888888889,"lng":-9.390277777777778,"alt":30.341211225997046},"data":"2017:08:26 20:38:56","taken_at":"2017-08-26T20:38:56.000Z","mime_type":"image/jpeg","width":3745,"height":1941,"size_bytes":468928,"mtimeMs":1772610987581.1472,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 FY67","city":"West Clare Municipal District","county_code":"CE","address":"Aille River","timezone":"Europe/Dublin","time":"+01:00"}},"2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070":{"id":"2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0214.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0214_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0214_avg.jpg","gps":{"lat":53.013308333333335,"lng":-9.3872,"alt":19.450097847358123},"data":"2017:08:27 08:23:04","taken_at":"2017-08-27T08:23:04.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2097953,"mtimeMs":1772610987584.4805,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 FY67","city":"Fitz's Cross","county_code":"CE","address":"Fisherman's Rest","timezone":"Europe/Dublin","time":"+01:00"}},"bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba":{"id":"bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0215.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0215_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0215_avg.jpg","gps":{"lat":53.0132,"lng":-9.38715,"alt":18.498759305210918},"data":"2017:08:27 08:38:10","taken_at":"2017-08-27T08:38:10.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2489649,"mtimeMs":1772610987584.4805,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","postcode":"V95 FY67","city":"Fitz's Cross","county_code":"CE","address":"Fisherman's Rest","timezone":"Europe/Dublin","time":"+01:00"}},"a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9":{"id":"a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0216.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0216_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0216_avg.jpg","gps":{"lat":53.04830833333333,"lng":-9.139725,"alt":140.78151260504202},"data":"2017:08:27 10:47:31","taken_at":"2017-08-27T10:47:31.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3009764,"mtimeMs":1772610987587.8137,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","city":"West Clare Municipal District","county_code":"CE","address":"The Portal Tomb","timezone":"Europe/Dublin","time":"+01:00"}},"915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f":{"id":"915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0217.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0217_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0217_avg.jpg","gps":{"lat":53.04877777777778,"lng":-9.139919444444445,"alt":142.0483870967742},"data":"2017:08:27 10:49:03","taken_at":"2017-08-27T10:49:03.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2757600,"mtimeMs":1772610987591.147,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","city":"West Clare Municipal District","county_code":"CE","address":"Poulnabrone Dolmen","timezone":"Europe/Dublin","time":"+01:00"}},"0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82":{"id":"0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0218.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0218_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0218_avg.jpg","gps":{"lat":53.048788888888886,"lng":-9.139975,"alt":142.04854368932038},"data":"2017:08:27 10:49:10","taken_at":"2017-08-27T10:49:10.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3442902,"mtimeMs":1772610987594.4802,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"West Clare Municipal District","city":"West Clare Municipal District","county_code":"CE","address":"Poulnabrone Dolmen","timezone":"Europe/Dublin","time":"+01:00"}},"a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9":{"id":"a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0223.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0223_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0223_avg.jpg","gps":{"lat":53.07643055555556,"lng":-9.159872222222223,"alt":100.63625304136254},"data":"2017:08:27 11:29:15","taken_at":"2017-08-27T11:29:15.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3891743,"mtimeMs":1772610987597.8135,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Clare","county_code":"CE","address":"Corkscrew Hill","timezone":"Europe/Dublin","time":"+01:00"}},"308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df":{"id":"308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0227.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0227_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0227_avg.jpg","gps":{"lat":53.27153055555556,"lng":-9.053811111111111,"alt":12.426332288401253},"data":"2017:08:27 14:30:08","taken_at":"2017-08-27T14:30:08.000Z","mime_type":"image/jpeg","width":3162,"height":1743,"size_bytes":1551495,"mtimeMs":1772610987597.8135,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Connacht","postcode":"H91 XP9Y","city":"Galway","county_code":"G","address":"Avenue","timezone":"Europe/Dublin","time":"+01:00"}},"20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a":{"id":"20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0228.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0228_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0228_avg.jpg","gps":{"lat":53.48401666666667,"lng":-9.977619444444445,"alt":40.73868882733149},"data":"2017:08:27 18:13:44","taken_at":"2017-08-27T18:13:44.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1257412,"mtimeMs":1772610987601.147,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Conamara Municipal District","postcode":"H71 AK52","city":"Conamara Municipal District","county_code":"G","address":"Cregg House","timezone":"Europe/Dublin","time":"+01:00"}},"37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687":{"id":"37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0229.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0229_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0229_avg.jpg","gps":{"lat":53.488727777777775,"lng":-10.022394444444446,"alt":16.373895976447496},"data":"2017:08:27 19:42:29","taken_at":"2017-08-27T19:42:29.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2405533,"mtimeMs":1772610987601.147,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Conamara Municipal District","postcode":"H71 Y277","city":"Clifden","county_code":"G","address":"Vivian","timezone":"Europe/Dublin","time":"+01:00"}},"1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7":{"id":"1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0232.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0232_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0232_avg.jpg","gps":{"lat":53.48798888888889,"lng":-10.010555555555555,"alt":17.364317841079462},"data":"2017:08:28 08:43:03","taken_at":"2017-08-28T08:43:03.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":2066043,"mtimeMs":1772610987604.4802,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Conamara Municipal District","postcode":"H71 AK52","city":"Clifden","county_code":"G","address":"Burkes B & B","timezone":"Europe/Dublin","time":"+01:00"}},"6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b":{"id":"6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0233.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0233_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0233_avg.jpg","gps":{"lat":53.5616,"lng":-9.917430555555555,"alt":25.366141732283463},"data":"2017:08:28 10:38:47","taken_at":"2017-08-28T10:38:47.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4708675,"mtimeMs":1772610987611.1467,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Conamara Municipal District","postcode":"H91 K224","city":"Conamara Municipal District","county_code":"G","address":"Dawros River","timezone":"Europe/Dublin","time":"+01:00"}},"6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc":{"id":"6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0234.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0234_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0234_avg.jpg","gps":{"lat":53.56163055555555,"lng":-9.917408333333332,"alt":25.604172461752434},"data":"2017:08:28 10:38:53","taken_at":"2017-08-28T10:38:53.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4888844,"mtimeMs":1772610987614.48,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Conamara Municipal District","postcode":"H91 K224","city":"Conamara Municipal District","county_code":"G","address":"Dawros River","timezone":"Europe/Dublin","time":"+01:00"}},"e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2":{"id":"e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0235.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0235_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0235_avg.jpg","gps":{"lat":53.56167222222222,"lng":-9.917269444444443,"alt":25.246113989637305},"data":"2017:08:28 10:39:10","taken_at":"2017-08-28T10:39:10.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4413193,"mtimeMs":1772610987617.8132,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Conamara Municipal District","postcode":"H91 K224","city":"Conamara Municipal District","county_code":"G","address":"Dawros River","timezone":"Europe/Dublin","time":"+01:00"}},"9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b":{"id":"9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0237.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0237_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0237_avg.jpg","gps":{"lat":53.56166944444444,"lng":-9.917263888888888,"alt":25.190361445783132},"data":"2017:08:28 10:39:24","taken_at":"2017-08-28T10:39:24.000Z","mime_type":"image/jpeg","width":3630,"height":3790,"size_bytes":3314673,"mtimeMs":1772610987621.1467,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Conamara Municipal District","postcode":"H91 K224","city":"Conamara Municipal District","county_code":"G","address":"Dawros River","timezone":"Europe/Dublin","time":"+01:00"}},"3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27":{"id":"3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0241.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0241_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0241_avg.jpg","gps":{"lat":53.419936111111106,"lng":-6.940597222222222,"alt":72.56199677938808},"data":"2017:08:29 10:15:22","taken_at":"2017-08-29T10:15:22.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1745323,"mtimeMs":1772610987624.48,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Clane — Maynooth","city":"The Municipal District of Clane — Maynooth","county_code":"KE","address":"Ballna Bog","timezone":"Europe/Dublin","time":"+01:00"}},"b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc":{"id":"b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0242.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0242_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0242_avg.jpg","gps":{"lat":53.18601111111111,"lng":-6.879438888888889,"alt":120.1146953405018},"data":"2017:08:29 11:17:46","taken_at":"2017-08-29T11:17:46.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":1242828,"mtimeMs":1772610987624.48,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kildare","city":"The Municipal District of Kildare — Newbridge","county_code":"KE","address":"Rathbride House","timezone":"Europe/Dublin","time":"+01:00"}},"b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9":{"id":"b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0243.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0243_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0243_avg.jpg","gps":{"lat":53.18579999999999,"lng":-6.8796083333333335,"alt":111.82454128440367},"data":"2017:08:29 11:18:06","taken_at":"2017-08-29T11:18:06.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3347118,"mtimeMs":1772610987627.8132,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"Kildare","city":"The Municipal District of Kildare — Newbridge","county_code":"KE","address":"Rathbride House","timezone":"Europe/Dublin","time":"+01:00"}},"b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020":{"id":"b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0244.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0244_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0244_avg.jpg","gps":{"lat":53.144641666666665,"lng":-6.901408333333333,"alt":83.13207547169812},"data":"2017:08:29 11:37:56","taken_at":"2017-08-29T11:37:56.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4832084,"mtimeMs":1772610987634.4797,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Kildare — Newbridge","postcode":"R51 AP20","city":"Tracey's Crossroads","county_code":"KE","address":"Japanese Gardens","timezone":"Europe/Dublin","time":"+01:00"}},"e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186":{"id":"e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0245.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0245_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0245_avg.jpg","gps":{"lat":53.14463333333333,"lng":-6.901205555555556,"alt":104.18667917448406},"data":"2017:08:29 11:38:52","taken_at":"2017-08-29T11:38:52.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4210606,"mtimeMs":1772610987637.813,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Kildare — Newbridge","postcode":"R51 AP20","city":"Tracey's Crossroads","county_code":"KE","address":"Japanese Gardens","timezone":"Europe/Dublin","time":"+01:00"}},"b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751":{"id":"b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0246.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0246_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0246_avg.jpg","gps":{"lat":53.14440833333333,"lng":-6.901238888888889,"alt":122.44324324324324},"data":"2017:08:29 11:40:11","taken_at":"2017-08-29T11:40:11.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4047020,"mtimeMs":1772610987644.4797,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Kildare — Newbridge","postcode":"R51 AP20","city":"Tracey's Crossroads","county_code":"KE","address":"Japanese Gardens","timezone":"Europe/Dublin","time":"+01:00"}},"d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030":{"id":"d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0247.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0247_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0247_avg.jpg","gps":{"lat":53.144486111111114,"lng":-6.901394444444445,"alt":101.99131944444444},"data":"2017:08:29 11:41:36","taken_at":"2017-08-29T11:41:36.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":3155785,"mtimeMs":1772610987647.813,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Kildare — Newbridge","postcode":"R51 AP20","city":"Tracey's Crossroads","county_code":"KE","address":"Japanese Gardens","timezone":"Europe/Dublin","time":"+01:00"}},"0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763":{"id":"0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0248.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0248_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0248_avg.jpg","gps":{"lat":53.14426111111111,"lng":-6.902130555555556,"alt":117.1704918032787},"data":"2017:08:29 11:45:12","taken_at":"2017-08-29T11:45:12.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4551844,"mtimeMs":1772610987651.1462,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Kildare — Newbridge","postcode":"R51 AP20","city":"Tracey's Crossroads","county_code":"KE","address":"Japanese Gardens","timezone":"Europe/Dublin","time":"+01:00"}},"10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457":{"id":"10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/IMG_0249.JPG","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0249_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/IMG_0249_avg.jpg","gps":{"lat":53.14420833333333,"lng":-6.902055555555556,"alt":90.47846889952153},"data":"2017:08:29 11:45:40","taken_at":"2017-08-29T11:45:40.000Z","mime_type":"image/jpeg","width":4032,"height":3024,"size_bytes":4772193,"mtimeMs":1772610987654.4795,"duration":null,"location":{"continent":"Europe","country":"Ireland","region":"The Municipal District of Kildare — Newbridge","postcode":"R51 AP20","city":"Tracey's Crossroads","county_code":"KE","address":"Japanese Gardens","timezone":"Europe/Dublin","time":"+01:00"}},"e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1":{"id":"e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1","user":"Fabio","cartella":"2017Irlanda19-29ago","path":"/photos/Fabio/2017Irlanda19-29ago/VID_20260221_095917.mp4","thub1":"/photos/Fabio/thumbs/2017Irlanda19-29ago/VID_20260221_095917_min.jpg","thub2":"/photos/Fabio/thumbs/2017Irlanda19-29ago/VID_20260221_095917_avg.jpg","gps":{"lat":44.1816,"lng":12.1251,"alt":null},"data":null,"taken_at":null,"mime_type":"video/mp4","width":1920,"height":1080,"size_bytes":12273606,"mtimeMs":1772610987667.8127,"duration":"5.472541","location":{"continent":"Europe","country":"Italy","region":"Emilia-Romagna","postcode":"47034","city":"Forlimpopoli","county_code":"FC","address":"Via Sandro Pertini, 99999/SN2","timezone":"Europe/Rome","time":"+01:00"}}} \ No newline at end of file