"""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,
)