Fix item visibility bugs and add effects web UI

- Fix recipe filter to allow owner=None (S-expression compiled recipes)
- Fix media uploads to use category (video/image/audio) not MIME type
- Fix IPFS imports to detect and store correct media type
- Add Effects navigation link between Recipes and Media
- Create effects list and detail templates with upload functionality
- Add cache/not_found.html template (was missing)
- Add type annotations to service classes
- Add tests for item visibility and effects web UI (30 tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-12 12:01:54 +00:00
parent 19e2277155
commit 585c75e846
12 changed files with 1090 additions and 53 deletions

View File

@@ -7,10 +7,14 @@ import json
import os
import subprocess
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple
from typing import Optional, List, Dict, Any, Tuple, TYPE_CHECKING
import httpx
if TYPE_CHECKING:
from database import Database
from cache_manager import L1CacheManager
def detect_media_type(cache_path: Path) -> str:
"""Detect if file is image, video, or audio based on magic bytes."""
@@ -86,7 +90,7 @@ class CacheService:
Handles content retrieval, metadata, and media type detection.
"""
def __init__(self, database, cache_manager):
def __init__(self, database: "Database", cache_manager: "L1CacheManager") -> None:
self.db = database
self.cache = cache_manager
self.cache_dir = Path(os.environ.get("CACHE_DIR", "/tmp/artdag-cache"))
@@ -293,10 +297,10 @@ class CacheService:
self,
cid: str,
actor_id: str,
title: str = None,
description: str = None,
tags: List[str] = None,
custom: Dict[str, Any] = None,
title: Optional[str] = None,
description: Optional[str] = None,
tags: Optional[List[str]] = None,
custom: Optional[Dict[str, Any]] = None,
) -> Tuple[bool, Optional[str]]:
"""Update content metadata. Returns (success, error)."""
if not self.cache.has_content(cid):
@@ -431,16 +435,19 @@ class CacheService:
if not ipfs_client.get_file(ipfs_cid, str(tmp_path)):
return None, f"Could not fetch CID {ipfs_cid} from IPFS"
# Store in cache
cached, ipfs_cid = self.cache.put(tmp_path, node_type="import", move=True)
cid = ipfs_cid or cached.cid # Prefer IPFS CID
# Detect media type before storing
media_type = detect_media_type(tmp_path)
# Save to database
await self.db.create_cache_item(cid, ipfs_cid)
# Store in cache
cached, new_ipfs_cid = self.cache.put(tmp_path, node_type="import", move=True)
cid = new_ipfs_cid or cached.cid # Prefer IPFS CID
# Save to database with detected media type
await self.db.create_cache_item(cid, new_ipfs_cid)
await self.db.save_item_metadata(
cid=cid,
actor_id=actor_id,
item_type="media",
item_type=media_type, # Use detected type for filtering
filename=f"ipfs-{ipfs_cid[:16]}"
)
@@ -463,19 +470,21 @@ class CacheService:
tmp.write(content)
tmp_path = Path(tmp.name)
# Detect MIME type before moving file
mime_type = get_mime_type(tmp_path)
# Detect media type (video/image/audio) before moving file
media_type = detect_media_type(tmp_path)
# Store in cache (also stores in IPFS)
cached, ipfs_cid = self.cache.put(tmp_path, node_type="upload", move=True)
cid = ipfs_cid or cached.cid # Prefer IPFS CID
# Save to database with detected MIME type
# Save to database with media category type
# Using media_type ("video", "image", "audio") not mime_type ("video/mp4")
# so list_media filtering works correctly
await self.db.create_cache_item(cid, ipfs_cid)
await self.db.save_item_metadata(
cid=cid,
actor_id=actor_id,
item_type=mime_type, # Store actual MIME type
item_type=media_type, # Store media category for filtering
filename=filename
)
@@ -485,11 +494,11 @@ class CacheService:
async def list_media(
self,
actor_id: str = None,
username: str = None,
actor_id: Optional[str] = None,
username: Optional[str] = None,
offset: int = 0,
limit: int = 24,
media_type: str = None,
media_type: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""List media items in cache."""
# Get items from database (uses item_types table)