diff options
Diffstat (limited to 'server.mjs')
| -rw-r--r-- | server.mjs | 144 |
1 files changed, 93 insertions, 51 deletions
@@ -37,76 +37,102 @@ async function load() { FROM photos p JOIN files f ON f.photo_id = p.id AND f.file_primary = 1 AND f.file_missing = 0 WHERE p.deleted_at IS NULL - ORDER BY p.taken_at_local` + ORDER BY p.taken_at_local, p.id` ) const byId = new Map() + const photoOrder = [] for (const p of photos) { byId.set(p.id, { id: p.id, thumb: thumb(p.file_hash), + full: thumb(p.file_hash, BIG_SIZE), title: p.photo_title || "", date: p.photo_year ? `${p.photo_year}-${String(p.photo_month || 1).padStart(2, "0")}-${String(p.photo_day || 1).padStart(2, "0")}` : "", }) + photoOrder.push(p.id) } + const rank = new Map(photoOrder.map((id, i) => [id, i])) - const facetQueries = [ - { - type: "label", - label: "Labels", - q: `SELECT pl.label_id AS k, l.label_name AS name, COUNT(*) AS n - FROM photos_labels pl JOIN labels l ON l.id = pl.label_id - GROUP BY pl.label_id HAVING n >= ? ORDER BY n DESC`, - member: `SELECT label_id AS k, photo_id FROM photos_labels`, - }, - { - type: "keyword", - label: "Keywords", - q: `SELECT pk.keyword_id AS k, kw.keyword AS name, COUNT(*) AS n - FROM photos_keywords pk JOIN keywords kw ON kw.id = pk.keyword_id - GROUP BY pk.keyword_id HAVING n >= ? ORDER BY n DESC`, - member: `SELECT keyword_id AS k, photo_id FROM photos_keywords`, - }, - { - type: "camera", - label: "Cameras", - q: `SELECT p.camera_id AS k, CONCAT(c.camera_make, ' ', c.camera_model) AS name, COUNT(*) AS n - FROM photos p JOIN cameras c ON c.id = p.camera_id - WHERE p.camera_id <> 1 - GROUP BY p.camera_id HAVING n >= ? ORDER BY n DESC`, - member: `SELECT camera_id AS k, id AS photo_id FROM photos WHERE camera_id <> 1`, - }, - { - type: "lens", - label: "Lenses", - q: `SELECT p.lens_id AS k, l.lens_model AS name, COUNT(*) AS n - FROM photos p JOIN lenses l ON l.id = p.lens_id - WHERE p.lens_id <> 1 - GROUP BY p.lens_id HAVING n >= ? ORDER BY n DESC`, - member: `SELECT lens_id AS k, id AS photo_id FROM photos WHERE lens_id <> 1`, - }, - ] + const paths = new Map() + const photoPaths = new Map() - const facets = [] - for (const f of facetQueries) { - const [rows] = await pool.query(f.q, [MIN_PATH]) - const [members] = await pool.query(f.member) - const groups = new Map() - for (const m of members) { - if (!groups.has(m.k)) groups.set(m.k, []) - groups.get(m.k).push(m.photo_id) - } + function addRows(rows, type, label) { + const grouped = new Map() for (const r of rows) { - const ids = (groups.get(r.k) || []).filter((id) => byId.has(id)) - if (ids.length >= MIN_PATH) { - facets.push({ type: f.type, label: f.label, name: r.name, count: ids.length, ids }) + if (!byId.has(r.pid)) continue + if (!grouped.has(r.k)) grouped.set(r.k, { key: `${type}:${r.k}`, type, label, name: r.name, ids: [] }) + grouped.get(r.k).ids.push(r.pid) + } + for (const g of grouped.values()) { + g.ids.sort((a, b) => rank.get(a) - rank.get(b)) + g.count = g.ids.length + paths.set(g.key, g) + for (const pid of g.ids) { + if (!photoPaths.has(pid)) photoPaths.set(pid, []) + photoPaths.get(pid).push(g) } } } - return { builtAt: new Date().toISOString(), photos: Object.fromEntries(byId), facets } + const [lab] = await pool.query( + `SELECT pl.label_id AS k, l.label_name AS name, pl.photo_id AS pid + FROM photos_labels pl JOIN labels l ON l.id = pl.label_id`) + addRows(lab, "label", "Labels") + + const [kw] = await pool.query( + `SELECT pk.keyword_id AS k, kw.keyword AS name, pk.photo_id AS pid + FROM photos_keywords pk JOIN keywords kw ON kw.id = pk.keyword_id`) + addRows(kw, "keyword", "Keywords") + + const [cam] = await pool.query( + `SELECT p.camera_id AS k, CONCAT(c.camera_make, ' ', c.camera_model) AS name, p.id AS pid + FROM photos p JOIN cameras c ON c.id = p.camera_id + WHERE p.camera_id <> 1 AND p.camera_id IS NOT NULL`) + addRows(cam, "camera", "Cameras") + + const [len] = await pool.query( + `SELECT p.lens_id AS k, l.lens_model AS name, p.id AS pid + FROM photos p JOIN lenses l ON l.id = p.lens_id + WHERE p.lens_id <> 1 AND p.lens_id IS NOT NULL`) + addRows(len, "lens", "Lenses") + + const facets = [] + for (const type of ["label", "keyword", "camera", "lens"]) { + const list = Array.from(paths.values()) + .filter((g) => g.type === type && g.count >= MIN_PATH) + .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)) + for (const g of list) facets.push({ type: g.type, label: g.label, name: g.name, count: g.count, ids: g.ids }) + } + + const library = { key: "library:all", type: "library", label: "Library", name: "library", ids: photoOrder, count: photoOrder.length } + const DIRS = ["N", "E", "S", "W"] + const exits = new Map() + for (const pid of photoOrder) { + const own = (photoPaths.get(pid) || []) + .filter((p) => p.count >= 2) + .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name) || a.type.localeCompare(b.type)) + const chosen = own.slice(0, 4) + while (chosen.length < 4) chosen.push(library) + const used = new Map() + const list = [] + for (let i = 0; i < 4; i++) { + const p = chosen[i] + const step = used.get(p.key) || 0 + used.set(p.key, step + 1) + const idx = p.ids.indexOf(pid) + list.push({ + dir: DIRS[i], + path: { type: p.type, name: p.name, count: p.count }, + nextId: p.ids[(idx + 1 + step) % p.ids.length], + }) + } + exits.set(pid, list) + } + + return { builtAt: new Date().toISOString(), photos: Object.fromEntries(byId), facets, photoOrder, byId, exits } } async function refresh() { @@ -193,7 +219,23 @@ const server = http.createServer(async (req, res) => { try { if (path === "/api/paths") { if (!state) await refresh() - return json(res, 200, state) + return json(res, 200, { builtAt: state.builtAt, photos: state.photos, facets: state.facets }) + } + if (path === "/api/random") { + if (!state) await refresh() + const id = state.photoOrder[Math.floor(Math.random() * state.photoOrder.length)] + return json(res, 200, { photo: state.byId.get(id) }) + } + const exitMatch = path.match(/^\/api\/exits\/(\d+)$/) + if (exitMatch) { + if (!state) await refresh() + const id = Number(exitMatch[1]) + const e = state.exits.get(id) + if (!e) return json(res, 404, { error: "not found" }) + return json(res, 200, { + photo: state.byId.get(id), + exits: e.map((x) => ({ dir: x.dir, path: x.path, next: state.byId.get(x.nextId) })), + }) } const photoMatch = path.match(/^\/api\/photo\/(\d+)$/) if (photoMatch) { |
