Render curated date-folder photos with title date and place captions
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
"""Interpret user-curated date folders without guessing dates from image files."""
|
||||
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
_DATE = r"(?P<year>\d{4})(?:[-._年]?)(?P<month>\d{2})(?:[-._月]?)(?P<day>\d{2})(?:日)?(?!\d)"
|
||||
_PREFIX = re.compile(r"^" + _DATE)
|
||||
|
||||
|
||||
def folder_caption(name: str) -> dict[str, str] | None:
|
||||
match = _PREFIX.match(name.strip())
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
captured = date(int(match["year"]), int(match["month"]), int(match["day"]))
|
||||
except ValueError:
|
||||
return None
|
||||
remainder = name.strip()[match.end() :].strip(" _-—–::")
|
||||
title, separator, place = remainder.replace("@", "@").partition("@")
|
||||
return {
|
||||
"title": " ".join(title.split()),
|
||||
"date": captured.isoformat(),
|
||||
"place": " ".join(place.split()) if separator else "",
|
||||
}
|
||||
|
||||
|
||||
def path_caption(path: str) -> dict[str, str] | None:
|
||||
# Children inherit their nearest date folder, rather than their filename.
|
||||
for component in reversed(PurePosixPath(path).parent.parts):
|
||||
caption = folder_caption(component)
|
||||
if caption is not None:
|
||||
return caption
|
||||
return None
|
||||
@@ -17,6 +17,7 @@ from pydantic import Field
|
||||
from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
from pigway_frame_flow.models import StrictModel
|
||||
from pigway_frame_flow.providers.photo_names import path_caption
|
||||
from pigway_frame_flow.settings import secret
|
||||
from pigway_frame_flow.storage.database import encode
|
||||
|
||||
@@ -27,6 +28,9 @@ class PhotoSourceInput(StrictModel):
|
||||
location: str = Field(min_length=1, max_length=2048)
|
||||
recursive: bool = True
|
||||
enabled: bool = True
|
||||
catalog_limit: int = Field(default=10000, ge=1, le=100000)
|
||||
organized_only: bool = False
|
||||
folder_caption: bool = False
|
||||
credential_env: str | None = Field(
|
||||
default=None, pattern=r"^FRAME_FLOW_SOURCE_[A-Z0-9_]+$"
|
||||
)
|
||||
@@ -39,6 +43,7 @@ class PhotoSourceView(PhotoSourceInput):
|
||||
class PhotoItem(StrictModel):
|
||||
id: str
|
||||
name: str
|
||||
caption: dict[str, str] | None = None
|
||||
|
||||
|
||||
class PhotoCatalog(StrictModel):
|
||||
@@ -204,13 +209,19 @@ class PhotoSources:
|
||||
sorted(
|
||||
d
|
||||
for d in dirs
|
||||
if not d.startswith(".")
|
||||
if d != "#recycle"
|
||||
and not d.startswith(".")
|
||||
and not d.startswith("@")
|
||||
and not Path(folder, d).is_symlink()
|
||||
)
|
||||
if source.recursive
|
||||
else []
|
||||
)
|
||||
caption = path_caption(
|
||||
Path(folder).relative_to(root).as_posix() + "/__photo__.jpg"
|
||||
)
|
||||
if source.organized_only and caption is None:
|
||||
continue
|
||||
for name in sorted(files):
|
||||
path = Path(folder, name)
|
||||
if (
|
||||
@@ -223,12 +234,18 @@ class PhotoSources:
|
||||
if path.stat().st_size > self.settings.max_bytes:
|
||||
continue
|
||||
relative = path.relative_to(root).as_posix()
|
||||
items.append({"id": relative, "name": name})
|
||||
items.append(
|
||||
{
|
||||
"id": relative,
|
||||
"name": name,
|
||||
"caption": caption if source.folder_caption else None,
|
||||
}
|
||||
)
|
||||
except OSError:
|
||||
continue
|
||||
if len(items) >= 10000:
|
||||
if len(items) >= source.catalog_limit:
|
||||
break
|
||||
if len(items) >= 10000:
|
||||
if len(items) >= source.catalog_limit:
|
||||
break
|
||||
else:
|
||||
data = self.assets.fetch_url(
|
||||
@@ -266,12 +283,19 @@ class PhotoSources:
|
||||
or parsed.fragment
|
||||
):
|
||||
continue
|
||||
caption = path_caption(unquote(parsed.path))
|
||||
if source.organized_only and caption is None:
|
||||
continue
|
||||
if Path(parsed.path).suffix.lower() in self.extensions:
|
||||
seen.add(url)
|
||||
items.append(
|
||||
{"id": url, "name": unquote(Path(parsed.path).name)}
|
||||
{
|
||||
"id": url,
|
||||
"name": unquote(Path(parsed.path).name),
|
||||
"caption": caption if source.folder_caption else None,
|
||||
}
|
||||
)
|
||||
if len(items) >= 10000:
|
||||
if len(items) >= source.catalog_limit:
|
||||
break
|
||||
except ElementTree.ParseError:
|
||||
raise FlowError(E.INVALID_REQUEST, "Invalid WebDAV catalog") from None
|
||||
@@ -332,4 +356,4 @@ class PhotoSources:
|
||||
data = self.assets.fetch_url(item["id"], headers=self.headers(source))
|
||||
if len(data) > self.settings.max_bytes:
|
||||
raise FlowError(E.INVALID_REQUEST, "Image exceeds size limit")
|
||||
return data, item["name"]
|
||||
return data, item["name"], item.get("caption")
|
||||
|
||||
Reference in New Issue
Block a user