feat(metadata): read from NC Memories first, ExifTool subprocess as fallback
Phase 3 (fat refactor). extract_metadata now tries Memories'
HTTP API GET /index.php/apps/memories/api/image/info/{fileid}
before spawning ExifTool. Replaces ~80–100 ms of subprocess work
with a ~1–2 ms HTTP call for ongoing imports.
What we kept from the ExifTool path:
- Mule's date-fallback chain (SubSec → DateTimeOriginal → CreateDate
→ MediaCreateDate → TrackCreateDate → filename/folder guess → mtime).
Memories' single `datetaken` field falls back to mtime, which would
silently mis-date the 6k+ photos in our library that depend on
filename-encoded dates. _apply_memories_metadata re-applies the
same chain against Memories' `exif` dict.
- taken_at_source='manual' is still sacred — never overwritten.
- has_date_warning recomputed against the resolved taken_at.
Format compat: Memories' `exif` dict uses plain key names (Make,
Model, ISO, FNumber, DateTimeOriginal, GPSLatitude, ...) while the
old ExifTool path stored `EXIF:Make` etc. PhotoInfoPanel only reads
the four keys above and Memories has them in plain form, so the info
panel keeps working without an adapter. Full-text search (ILIKE on
exif_json) still hits camera names, lens names, dates etc. — value
content is identical, only the keys differ.
Fallback paths preserved:
- 404 from Memories (file not yet indexed by NC's scan, brand-new
upload): falls through to ExifTool.
- non-NC photos (no nextcloud_fileid or no app password): ExifTool.
- NC HTTP error or parse failure: ExifTool.
CSRF: Memories' /api/image/info/{id} is CSRF-checked. We send
`OCS-APIRequest: true` to bypass it, the same way the OCS clients
do. Auth is the user's existing Fernet-encrypted app password.
Verified end-to-end against:
- IMG_4954.DNG (real DNG with GPS): width/height/lat/lon/taken_at
match the previous ExifTool output exactly; exif_json switched
to Memories format (Make/Model/ISO/FNumber preserved).
- 20210817_000000_4A6737B6.jpg (path-dated archive photo): taken_at
remained 2021-08-17 from the filename heuristic, source='path'.
The `enabled` state of the Memories app is now required for new
imports to skip ExifTool — left enabled in commit 0a4c8d... (NC
admin action; not in this commit).
This commit is contained in:
@@ -344,6 +344,53 @@ def fetch_fileid(user: User, abs_path: str) -> Optional[int]:
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
|
||||
async def fetch_memories_info_async(
|
||||
user: User, fileid: int
|
||||
) -> Optional[dict]:
|
||||
"""Fetch the Memories app's per-file metadata blob.
|
||||
|
||||
`GET /index.php/apps/memories/api/image/info/{fileid}` returns
|
||||
Memories' pre-extracted view of the file: `w`, `h`, `datetaken`
|
||||
(unix epoch), `mtime`, `mimetype`, `size`, plus an `exif` dict
|
||||
of plain-named EXIF fields (Make, Model, ISO, FNumber,
|
||||
DateTimeOriginal, GPSLatitude, GPSLongitude, etc.). The endpoint
|
||||
is `#[NoAdminRequired] #[PublicPage]` but CSRF-checked, so we
|
||||
send `OCS-APIRequest: true` to bypass the check the same way
|
||||
OCS API clients do.
|
||||
|
||||
Returns None on 404 (file not yet indexed by Memories, or
|
||||
fileid stale) or any non-success response — callers should fall
|
||||
back to ExifTool extraction in that case.
|
||||
"""
|
||||
nc_user, app_pw = _credentials_for(user)
|
||||
url = (
|
||||
f"{_base_url()}/index.php/apps/memories/api/image/info/{int(fileid)}"
|
||||
)
|
||||
client = _async_client((nc_user, app_pw))
|
||||
try:
|
||||
resp = await client.get(
|
||||
url,
|
||||
headers={
|
||||
"OCS-APIRequest": "true",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
finally:
|
||||
await client.aclose()
|
||||
if resp.status_code == 404:
|
||||
return None
|
||||
if not resp.is_success:
|
||||
logger.warning(
|
||||
"Memories info for fileid %s returned %s", fileid, resp.status_code
|
||||
)
|
||||
return None
|
||||
try:
|
||||
return resp.json()
|
||||
except Exception as e:
|
||||
logger.warning("Memories info parse failed for fileid %s: %s", fileid, e)
|
||||
return None
|
||||
|
||||
|
||||
async def get_preview_async(
|
||||
user: User, fileid: int, x: int, y: int
|
||||
) -> httpx.Response:
|
||||
|
||||
Reference in New Issue
Block a user