b1: api — /img/{id}?i=n gallery chain (mirrored file -> CDN 302 -> 404; i=0 legacy), payload n_photos + last_photo_idx on deck/list/listing/section rows, POST /api/swipe persists optional photo_idx
This commit is contained in:
108
app/api.py
108
app/api.py
@@ -12,6 +12,9 @@ on; frozen in the task brief):
|
||||
url, img ("/img/{id}" if img_url or cached file exists else null), desc,
|
||||
family, score, year, seller_type, extras (array of strings),
|
||||
decision (null|'like'|'maybe'|'nope'), first_seen}
|
||||
B1 additions (UI-SPEC §11 / DATA-MODEL "B1 additions"):
|
||||
n_photos (int), last_photo_idx (int|null); GET /img/{id}?i=n serves
|
||||
gallery photo n.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
@@ -42,7 +45,7 @@ DECK_WHERE = (
|
||||
)
|
||||
|
||||
DECK_SELECT = (
|
||||
"SELECT l.*, s.action AS decision"
|
||||
"SELECT l.*, s.action AS decision, s.last_photo_idx AS last_photo_idx"
|
||||
" FROM listings l LEFT JOIN swipes s ON s.listing_id = l.id"
|
||||
" WHERE " + DECK_WHERE
|
||||
)
|
||||
@@ -128,10 +131,21 @@ def count_deck_after(app, score, first_seen, listing_id):
|
||||
|
||||
def build_payload(img_dir):
|
||||
def _payload(conn, row):
|
||||
img = None
|
||||
if row['img_url'] or os.path.exists(
|
||||
os.path.join(img_dir, '%d.webp' % row['id'])):
|
||||
img = '/img/%d' % row['id']
|
||||
leg = os.path.join(img_dir, '%d.webp' % row['id'])
|
||||
b1 = os.path.join(img_dir, '%d_0.webp' % row['id'])
|
||||
has_photo = (bool(row['img_url']) or os.path.exists(leg)
|
||||
or os.path.exists(b1))
|
||||
img = '/img/%d' % row['id'] if has_photo else None
|
||||
# B1: gallery size + swipe-time photo index (UI-SPEC §11 / DATA-MODEL B1)
|
||||
imgs = []
|
||||
if isinstance(row['imgs'], str) and row['imgs']:
|
||||
try:
|
||||
imgs = json.loads(row['imgs'])
|
||||
except ValueError:
|
||||
imgs = []
|
||||
if not isinstance(imgs, list):
|
||||
imgs = []
|
||||
n_photos = len(imgs) if imgs else (1 if has_photo else 0)
|
||||
extras = row['extras']
|
||||
if isinstance(extras, str):
|
||||
try:
|
||||
@@ -148,6 +162,9 @@ def build_payload(img_dir):
|
||||
'region': row['region'],
|
||||
'url': row['url'],
|
||||
'img': img,
|
||||
'n_photos': n_photos,
|
||||
'last_photo_idx': (row['last_photo_idx']
|
||||
if 'last_photo_idx' in row.keys() else None),
|
||||
'desc': row['desc'],
|
||||
'family': row['family'],
|
||||
'score': row['score'],
|
||||
@@ -290,6 +307,15 @@ def create_app(db_path, img_dir, state_dir=None):
|
||||
section_id = body.get('section_id')
|
||||
if not isinstance(section_id, int):
|
||||
section_id = None
|
||||
# B1: optional photo index on screen at swipe time (§11.5 rotation).
|
||||
has_photo_idx = 'photo_idx' in body
|
||||
photo_idx = body.get('photo_idx')
|
||||
if has_photo_idx and photo_idx is not None:
|
||||
if (isinstance(photo_idx, bool) or not isinstance(photo_idx, int)
|
||||
or photo_idx < 0):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail='photo_idx must be a non-negative int (or null)')
|
||||
conn = get_conn(app)
|
||||
try:
|
||||
row = conn.execute('SELECT id FROM listings WHERE id=?',
|
||||
@@ -297,19 +323,32 @@ def create_app(db_path, img_dir, state_dir=None):
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail='unknown listing')
|
||||
now = imp.now_iso()
|
||||
conn.execute(
|
||||
'''INSERT INTO swipes (listing_id, action, created_at, updated_at)
|
||||
VALUES (?,?,?,?)
|
||||
ON CONFLICT(listing_id) DO UPDATE SET
|
||||
action=excluded.action, updated_at=excluded.updated_at''',
|
||||
(listing_id, action, now, now))
|
||||
if has_photo_idx:
|
||||
# explicit photo_idx (int or null) overwrites; an absent key
|
||||
# preserves the stored index (list edits etc.)
|
||||
conn.execute(
|
||||
'''INSERT INTO swipes (listing_id, action, created_at,
|
||||
updated_at, last_photo_idx)
|
||||
VALUES (?,?,?,?,?)
|
||||
ON CONFLICT(listing_id) DO UPDATE SET
|
||||
action=excluded.action, updated_at=excluded.updated_at,
|
||||
last_photo_idx=excluded.last_photo_idx''',
|
||||
(listing_id, action, now, now, photo_idx))
|
||||
else:
|
||||
conn.execute(
|
||||
'''INSERT INTO swipes (listing_id, action, created_at, updated_at)
|
||||
VALUES (?,?,?,?)
|
||||
ON CONFLICT(listing_id) DO UPDATE SET
|
||||
action=excluded.action, updated_at=excluded.updated_at''',
|
||||
(listing_id, action, now, now))
|
||||
# P3: append the learning event (round = prior events + 1)
|
||||
round_ = dbmod.append_event(conn, listing_id, action, section_id,
|
||||
now)
|
||||
conn.commit()
|
||||
stored = conn.execute(
|
||||
'SELECT listing_id, action, created_at, updated_at'
|
||||
' FROM swipes WHERE listing_id=?', (listing_id,)).fetchone()
|
||||
'SELECT listing_id, action, created_at, updated_at,'
|
||||
' last_photo_idx FROM swipes WHERE listing_id=?',
|
||||
(listing_id,)).fetchone()
|
||||
result = dict(stored)
|
||||
result['round'] = round_
|
||||
result['section_id'] = section_id
|
||||
@@ -353,7 +392,8 @@ def create_app(db_path, img_dir, state_dir=None):
|
||||
' LEFT JOIN swipes s ON s.listing_id = l.id'
|
||||
' WHERE ' + where, params).fetchone()['n']
|
||||
rows = conn.execute(
|
||||
'SELECT l.*, s.action AS decision FROM listings l'
|
||||
'SELECT l.*, s.action AS decision,'
|
||||
' s.last_photo_idx AS last_photo_idx FROM listings l'
|
||||
' LEFT JOIN swipes s ON s.listing_id = l.id'
|
||||
' WHERE ' + where + ' ORDER BY ' + LIST_SORTS[sort] +
|
||||
' LIMIT ? OFFSET ?', params + [limit, offset]).fetchall()
|
||||
@@ -368,7 +408,8 @@ def create_app(db_path, img_dir, state_dir=None):
|
||||
try:
|
||||
engine = secmod.build_engine(conn, now=utcnow())
|
||||
row = conn.execute(
|
||||
'SELECT l.*, s.action AS decision FROM listings l'
|
||||
'SELECT l.*, s.action AS decision,'
|
||||
' s.last_photo_idx AS last_photo_idx FROM listings l'
|
||||
' LEFT JOIN swipes s ON s.listing_id = l.id'
|
||||
' WHERE l.id=?', (listing_id,)).fetchone()
|
||||
if not row:
|
||||
@@ -625,18 +666,43 @@ def create_app(db_path, img_dir, state_dir=None):
|
||||
# -- images + static -------------------------------------------------
|
||||
|
||||
@app.get('/img/{listing_id}')
|
||||
def img(listing_id: int):
|
||||
fpath = os.path.join(app.state.img_dir, '%d.webp' % listing_id)
|
||||
def img(listing_id: int, i: int = Query(0, ge=0)):
|
||||
"""Photo n of a gallery (B1 §11.1): mirrored file -> CDN 302 -> 404.
|
||||
|
||||
i=0 keeps the legacy chain ({id}.webp, with {id}_0.webp accepted as
|
||||
the gallery-mirror name for the primary); i>0 serves {id}_{n}.webp
|
||||
with the CDN fallback taken from imgs[n].
|
||||
"""
|
||||
fpath = os.path.join(
|
||||
app.state.img_dir,
|
||||
'%d.webp' % listing_id if i == 0 else
|
||||
'%d_%d.webp' % (listing_id, i))
|
||||
if i == 0 and not os.path.exists(fpath):
|
||||
alt = os.path.join(app.state.img_dir, '%d_0.webp' % listing_id)
|
||||
if os.path.exists(alt):
|
||||
fpath = alt
|
||||
if os.path.exists(fpath):
|
||||
return FileResponse(fpath, media_type='image/webp')
|
||||
conn = get_conn(app)
|
||||
try:
|
||||
row = conn.execute('SELECT img_url FROM listings WHERE id=?',
|
||||
(listing_id,)).fetchone()
|
||||
row = conn.execute(
|
||||
'SELECT img_url, imgs FROM listings WHERE id=?',
|
||||
(listing_id,)).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
if row and row['img_url']:
|
||||
return RedirectResponse(url=row['img_url'], status_code=302)
|
||||
url = None
|
||||
if row:
|
||||
if i == 0 and row['img_url']:
|
||||
url = row['img_url']
|
||||
elif row['imgs']:
|
||||
try:
|
||||
arr = json.loads(row['imgs'])
|
||||
except ValueError:
|
||||
arr = []
|
||||
if isinstance(arr, list) and len(arr) > i and arr[i]:
|
||||
url = arr[i]
|
||||
if url:
|
||||
return RedirectResponse(url=url, status_code=302)
|
||||
raise HTTPException(status_code=404, detail='no image')
|
||||
|
||||
@app.get('/')
|
||||
|
||||
@@ -416,7 +416,8 @@ def close_section(conn, section, now, status='closed',
|
||||
|
||||
def section_items(conn, section):
|
||||
return conn.execute(
|
||||
"""SELECT l.*, s.action AS decision, si.slot, si.position
|
||||
"""SELECT l.*, s.action AS decision, s.last_photo_idx AS last_photo_idx,
|
||||
si.slot, si.position
|
||||
FROM section_items si
|
||||
JOIN listings l ON l.id = si.listing_id
|
||||
LEFT JOIN swipes s ON s.listing_id = l.id
|
||||
|
||||
Reference in New Issue
Block a user