aboutsummaryrefslogtreecommitdiff
path: root/server.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'server.mjs')
-rw-r--r--server.mjs144
1 files changed, 93 insertions, 51 deletions
diff --git a/server.mjs b/server.mjs
index 0c1dac6..d5479fb 100644
--- a/server.mjs
+++ b/server.mjs
@@ -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) {