- Fix Python formatting (sync no longer strips blank lines from .py files) - Remove test_community_build.py (tests private sync module) - Fix ruff warnings in test files - Add CODE_OF_CONDUCT.md - Add .github/PULL_REQUEST_TEMPLATE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
421 lines
16 KiB
Python
421 lines
16 KiB
Python
"""Media Handler - manages WordPress media library operations"""
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
import aiohttp
|
|
|
|
from plugins.wordpress.client import WordPressClient
|
|
|
|
|
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
|
"""Return tool specifications for ToolGenerator"""
|
|
return [
|
|
# === MEDIA ===
|
|
{
|
|
"name": "list_media",
|
|
"method_name": "list_media",
|
|
"description": "List media library items. Returns images, videos, documents with URLs and metadata.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"per_page": {
|
|
"type": "integer",
|
|
"description": "Number of media items per page",
|
|
"default": 20,
|
|
"minimum": 1,
|
|
"maximum": 100,
|
|
},
|
|
"page": {
|
|
"type": "integer",
|
|
"description": "Page number for pagination",
|
|
"default": 1,
|
|
"minimum": 1,
|
|
},
|
|
"media_type": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Filter by media type (image, video, audio, or application)",
|
|
"enum": ["image", "video", "audio", "application"],
|
|
},
|
|
},
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "get_media",
|
|
"method_name": "get_media",
|
|
"description": "Get detailed information about a media item. Returns full metadata including URLs, dimensions, and MIME type.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"media_id": {
|
|
"type": "integer",
|
|
"description": "Media ID to retrieve",
|
|
"minimum": 1,
|
|
}
|
|
},
|
|
"required": ["media_id"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "upload_media_from_url",
|
|
"method_name": "upload_media_from_url",
|
|
"description": "Upload media from URL to media library (sideload). Downloads file from public URL and uploads to WordPress.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"url": {
|
|
"type": "string",
|
|
"description": "Public URL of the media file to upload (image, video, document, etc.)",
|
|
},
|
|
"title": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Media title (used in media library)",
|
|
},
|
|
"alt_text": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Alternative text for accessibility (important for images)",
|
|
},
|
|
"caption": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Media caption (displayed below image when inserted into content)",
|
|
},
|
|
},
|
|
"required": ["url"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "update_media",
|
|
"method_name": "update_media",
|
|
"description": "Update media metadata. Supports title, description, slug, alt text, caption, status, and associated post.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"media_id": {
|
|
"type": "integer",
|
|
"description": "Media ID to update",
|
|
"minimum": 1,
|
|
},
|
|
"title": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Media title (displayed in media library)",
|
|
},
|
|
"description": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Media description (full text content, displayed in attachment page)",
|
|
},
|
|
"slug": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Media URL slug (e.g., 'my-image-name')",
|
|
},
|
|
"alt_text": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Alternative text for accessibility (important for images)",
|
|
},
|
|
"caption": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Media caption (shown below image in content)",
|
|
},
|
|
"status": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Publication status of the media",
|
|
"enum": ["publish", "draft", "private"],
|
|
},
|
|
"post": {
|
|
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
|
"description": "ID of the post/page to attach this media to",
|
|
},
|
|
},
|
|
"required": ["media_id"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "delete_media",
|
|
"method_name": "delete_media",
|
|
"description": "Delete media from library. Can permanently delete or move to trash.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"media_id": {
|
|
"type": "integer",
|
|
"description": "Media ID to delete",
|
|
"minimum": 1,
|
|
},
|
|
"force": {
|
|
"type": "boolean",
|
|
"description": "Permanently delete (true) or move to trash (false)",
|
|
"default": False,
|
|
},
|
|
},
|
|
"required": ["media_id"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
]
|
|
|
|
|
|
class MediaHandler:
|
|
"""Handle media-related operations for WordPress"""
|
|
|
|
def __init__(self, client: WordPressClient):
|
|
"""
|
|
Initialize media handler.
|
|
|
|
Args:
|
|
client: WordPress API client instance
|
|
"""
|
|
self.client = client
|
|
|
|
# === MEDIA ===
|
|
|
|
async def list_media(
|
|
self, per_page: int = 20, page: int = 1, media_type: str | None = None
|
|
) -> str:
|
|
"""
|
|
List media library items.
|
|
|
|
Args:
|
|
per_page: Number of media items per page (1-100)
|
|
page: Page number
|
|
media_type: Filter by media type (image, video, audio, application)
|
|
|
|
Returns:
|
|
JSON string with media list
|
|
"""
|
|
try:
|
|
params = {"per_page": per_page, "page": page}
|
|
if media_type:
|
|
params["media_type"] = media_type
|
|
|
|
media = await self.client.get("media", params=params)
|
|
|
|
# Format response
|
|
result = {
|
|
"total": len(media),
|
|
"page": page,
|
|
"per_page": per_page,
|
|
"media": [
|
|
{
|
|
"id": m["id"],
|
|
"title": m["title"]["rendered"],
|
|
"mime_type": m["mime_type"],
|
|
"media_type": m.get("media_type", ""),
|
|
"url": m["source_url"],
|
|
"date": m["date"],
|
|
"alt_text": m.get("alt_text", ""),
|
|
"link": m.get("link", ""),
|
|
}
|
|
for m in media
|
|
],
|
|
}
|
|
|
|
return json.dumps(result, indent=2)
|
|
except Exception as e:
|
|
return json.dumps(
|
|
{"error": str(e), "message": f"Failed to list media: {str(e)}"}, indent=2
|
|
)
|
|
|
|
async def get_media(self, media_id: int) -> str:
|
|
"""
|
|
Get detailed information about a specific media item.
|
|
|
|
Args:
|
|
media_id: Media ID to retrieve
|
|
|
|
Returns:
|
|
JSON string with media data
|
|
"""
|
|
try:
|
|
media = await self.client.get(f"media/{media_id}")
|
|
|
|
result = {
|
|
"id": media["id"],
|
|
"title": media["title"]["rendered"],
|
|
"mime_type": media["mime_type"],
|
|
"media_type": media.get("media_type", ""),
|
|
"url": media["source_url"],
|
|
"alt_text": media.get("alt_text", ""),
|
|
"caption": media.get("caption", {}).get("rendered", ""),
|
|
"description": media.get("description", {}).get("rendered", ""),
|
|
"date": media["date"],
|
|
"modified": media.get("modified", ""),
|
|
"link": media.get("link", ""),
|
|
"media_details": media.get("media_details", {}),
|
|
}
|
|
|
|
return json.dumps(result, indent=2)
|
|
except Exception as e:
|
|
return json.dumps(
|
|
{"error": str(e), "message": f"Failed to get media {media_id}: {str(e)}"}, indent=2
|
|
)
|
|
|
|
async def upload_media_from_url(
|
|
self,
|
|
url: str,
|
|
title: str | None = None,
|
|
alt_text: str | None = None,
|
|
caption: str | None = None,
|
|
) -> str:
|
|
"""
|
|
Upload media from URL to media library (sideload).
|
|
|
|
Downloads file from a public URL and uploads it to WordPress media library.
|
|
|
|
Args:
|
|
url: Public URL of the media file to upload
|
|
title: Media title (used in media library)
|
|
alt_text: Alternative text for accessibility
|
|
caption: Media caption (displayed below image)
|
|
|
|
Returns:
|
|
JSON string with uploaded media data
|
|
"""
|
|
try:
|
|
# Download file from URL
|
|
async with aiohttp.ClientSession() as session, session.get(url) as response:
|
|
if response.status >= 400:
|
|
raise Exception(f"Failed to download from URL: HTTP {response.status}")
|
|
|
|
file_content = await response.read()
|
|
content_type = response.headers.get("Content-Type", "application/octet-stream")
|
|
|
|
# Extract filename from URL
|
|
filename = url.split("/")[-1].split("?")[0]
|
|
if not filename:
|
|
filename = "downloaded_file"
|
|
|
|
# Create FormData for upload
|
|
form = aiohttp.FormData()
|
|
form.add_field("file", file_content, filename=filename, content_type=content_type)
|
|
|
|
# Upload to WordPress using client's upload method
|
|
# Note: We need to use the client's base_url and auth directly for file upload
|
|
upload_url = f"{self.client.base_url}/media"
|
|
headers = {
|
|
"Authorization": self.client.auth_header,
|
|
"Content-Disposition": f'attachment; filename="{filename}"',
|
|
}
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(upload_url, data=form, headers=headers) as response:
|
|
if response.status >= 400:
|
|
error_text = await response.text()
|
|
raise Exception(f"Upload failed (HTTP {response.status}): {error_text}")
|
|
|
|
media = await response.json()
|
|
|
|
# Update metadata if provided
|
|
if title or alt_text or caption:
|
|
update_data = {}
|
|
if title:
|
|
update_data["title"] = title
|
|
if alt_text:
|
|
update_data["alt_text"] = alt_text
|
|
if caption:
|
|
update_data["caption"] = caption
|
|
|
|
await self.client.post(f"media/{media['id']}", json_data=update_data)
|
|
|
|
result = {
|
|
"id": media["id"],
|
|
"title": media["title"]["rendered"],
|
|
"url": media["source_url"],
|
|
"mime_type": media["mime_type"],
|
|
"message": f"Media uploaded from URL successfully with ID {media['id']}",
|
|
}
|
|
|
|
return json.dumps(result, indent=2)
|
|
except Exception as e:
|
|
return json.dumps(
|
|
{"error": str(e), "message": f"Failed to upload media from URL: {str(e)}"}, indent=2
|
|
)
|
|
|
|
async def update_media(
|
|
self,
|
|
media_id: int,
|
|
title: str | None = None,
|
|
description: str | None = None,
|
|
slug: str | None = None,
|
|
alt_text: str | None = None,
|
|
caption: str | None = None,
|
|
status: str | None = None,
|
|
post: int | None = None,
|
|
) -> str:
|
|
"""
|
|
Update media metadata.
|
|
|
|
Args:
|
|
media_id: Media ID to update
|
|
title: Media title
|
|
description: Media description
|
|
slug: Media URL slug
|
|
alt_text: Alternative text for accessibility
|
|
caption: Media caption
|
|
status: Publication status (publish, draft, private)
|
|
post: ID of post/page to attach media to
|
|
|
|
Returns:
|
|
JSON string with updated media data
|
|
"""
|
|
try:
|
|
# Build data dict with only provided values
|
|
data = {}
|
|
if title is not None:
|
|
data["title"] = title
|
|
if description is not None:
|
|
data["description"] = description
|
|
if slug is not None:
|
|
data["slug"] = slug
|
|
if alt_text is not None:
|
|
data["alt_text"] = alt_text
|
|
if caption is not None:
|
|
data["caption"] = caption
|
|
if status is not None:
|
|
data["status"] = status
|
|
if post is not None:
|
|
data["post"] = post
|
|
|
|
media = await self.client.post(f"media/{media_id}", json_data=data)
|
|
|
|
result = {
|
|
"id": media["id"],
|
|
"title": media["title"]["rendered"],
|
|
"alt_text": media.get("alt_text", ""),
|
|
"caption": media.get("caption", {}).get("rendered", ""),
|
|
"url": media["source_url"],
|
|
"message": f"Media {media_id} updated successfully",
|
|
}
|
|
|
|
return json.dumps(result, indent=2)
|
|
except Exception as e:
|
|
return json.dumps(
|
|
{"error": str(e), "message": f"Failed to update media {media_id}: {str(e)}"},
|
|
indent=2,
|
|
)
|
|
|
|
async def delete_media(self, media_id: int, force: bool = False) -> str:
|
|
"""
|
|
Delete or trash media from library.
|
|
|
|
Args:
|
|
media_id: Media ID to delete
|
|
force: Permanently delete (True) or move to trash (False)
|
|
|
|
Returns:
|
|
JSON string with deletion result
|
|
"""
|
|
try:
|
|
params = {"force": "true" if force else "false"}
|
|
result = await self.client.delete(f"media/{media_id}", params=params)
|
|
|
|
message = f"Media {media_id} {'permanently deleted' if force else 'moved to trash'}"
|
|
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
|
|
except Exception as e:
|
|
return json.dumps(
|
|
{"error": str(e), "message": f"Failed to delete media {media_id}: {str(e)}"},
|
|
indent=2,
|
|
)
|