Skip to main content
Glama

Windows Operations MCP

media_metadata.py•10.1 kB
""" Media Metadata Tools This module provides functionality for reading and writing metadata from media files, including EXIF data from images and ID3 tags from MP3 files. """ import os from pathlib import Path from typing import Dict, Any, Optional, Union, List, Tuple from datetime import datetime from ..decorators import tool class MediaMetadataError(Exception): """Base exception for media metadata operations.""" pass # Try to import optional dependencies try: from PIL import Image, ExifTags # TAGS is available in ExifTags module in newer Pillow versions if hasattr(ExifTags, 'TAGS'): TAGS = ExifTags.TAGS else: # Fallback for older versions from PIL.Exif import TAGS HAS_PILLOW = True except ImportError: HAS_PILLOW = False TAGS = None try: import mutagen from mutagen.mp3 import MP3 from mutagen.id3 import ID3, TIT2, TPE1, TALB, TDRC, TCON, TRCK, TPE2, TCOM, TYER, TORY, TENC, TCOP, TSRC, TKEY, TBPM, COMM, USLT, SYLT, APIC, TXXX HAS_MUTAGEN = True except ImportError: HAS_MUTAGEN = False def _ensure_dependencies(): """Ensure required dependencies are installed.""" if not HAS_PILLOW: raise MediaMetadataError("Pillow is required for image metadata operations. Install with: pip install Pillow") if not HAS_MUTAGEN: raise MediaMetadataError("Mutagen is required for audio metadata operations. Install with: pip install mutagen") # EXIF (Image) Metadata Functions @tool( name="get_image_metadata", description="Get EXIF metadata from an image file", parameters={ "image_path": { "type": "string", "description": "Path to the image file" } } ) def get_image_metadata(image_path: Union[str, Path]) -> Dict[str, Any]: """ Get EXIF metadata from an image file. Args: image_path: Path to the image file Returns: Dictionary containing the image metadata Raises: MediaMetadataError: If the file is not an image or if there's an error reading metadata """ _ensure_dependencies() image_path = Path(image_path) if not image_path.exists(): raise MediaMetadataError(f"File not found: {image_path}") try: with Image.open(image_path) as img: exif_data = {} if hasattr(img, '_getexif') and img._getexif() is not None: for tag, value in img._getexif().items(): if tag in TAGS: exif_data[TAGS[tag]] = value return { "success": True, "metadata": exif_data, "file_info": { "format": img.format, "mode": img.mode, "size": img.size } } except Exception as e: raise MediaMetadataError(f"Error reading image metadata: {e}") def update_image_metadata( image_path: Union[str, Path], metadata: Dict[str, Any], save_copy: bool = False, output_path: Optional[Union[str, Path]] = None ) -> Dict[str, Any]: """ Update EXIF metadata for an image file. Args: image_path: Path to the image file metadata: Dictionary of metadata to update save_copy: If True, save to a new file instead of modifying the original output_path: Path to save the modified file (required if save_copy is True) Returns: Dictionary containing the updated metadata Raises: MediaMetadataError: If there's an error updating the metadata """ _ensure_dependencies() image_path = Path(image_path) if save_copy and not output_path: raise MediaMetadataError("output_path is required when save_copy is True") if not image_path.exists(): raise MediaMetadataError(f"File not found: {image_path}") try: with Image.open(image_path) as img: exif = img.info.get('exif', b'') # Create a copy of the image to modify if img.mode in ('P', 'PA'): img = img.convert('RGBA' if 'A' in img.mode else 'RGB') # Save the image with updated metadata output = output_path if save_copy else image_path img.save(output, exif=exif, **metadata) return get_image_metadata(output) except Exception as e: raise MediaMetadataError(f"Error updating image metadata: {e}") # MP3 (Audio) Metadata Functions def get_mp3_metadata(mp3_path: Union[str, Path]) -> Dict[str, Any]: """ Get ID3 metadata from an MP3 file. Args: mp3_path: Path to the MP3 file Returns: Dictionary containing the MP3 metadata Raises: MediaMetadataError: If the file is not an MP3 or if there's an error reading metadata """ _ensure_dependencies() mp3_path = Path(mp3_path) if not mp3_path.exists(): raise MediaMetadataError(f"File not found: {mp3_path}") try: audio = MP3(mp3_path, ID3=ID3) metadata = {} # Standard ID3 tags if audio.tags is not None: for frame_id, frame in audio.tags.items(): # Skip album art and other binary data if frame_id.startswith('APIC'): continue metadata[frame_id] = str(frame) # Audio info metadata.update({ 'bitrate': audio.info.bitrate // 1000, # kbps 'length': int(audio.info.length), # seconds 'channels': audio.info.channels, 'sample_rate': audio.info.sample_rate, }) return metadata except Exception as e: raise MediaMetadataError(f"Error reading MP3 metadata: {e}") def update_mp3_metadata( mp3_path: Union[str, Path], metadata: Dict[str, Any], save_copy: bool = False, output_path: Optional[Union[str, Path]] = None ) -> Dict[str, Any]: """ Update ID3 metadata for an MP3 file. Args: mp3_path: Path to the MP3 file metadata: Dictionary of metadata to update save_copy: If True, save to a new file instead of modifying the original output_path: Path to save the modified file (required if save_copy is True) Returns: Dictionary containing the updated metadata Raises: MediaMetadataError: If there's an error updating the metadata """ _ensure_dependencies() mp3_path = Path(mp3_path) if save_copy and not output_path: raise MediaMetadataError("output_path is required when save_copy is True") if not mp3_path.exists(): raise MediaMetadataError(f"File not found: {mp3_path}") try: # Create a copy if needed if save_copy: import shutil shutil.copy2(mp3_path, output_path) mp3_path = Path(output_path) # Update the metadata audio = MP3(mp3_path, ID3=ID3) if audio.tags is None: audio.add_tags() # Map common tag names to ID3 frame classes tag_map = { 'title': TIT2, 'artist': TPE1, 'album': TALB, 'date': TDRC, 'year': TYER, 'genre': TCON, 'tracknumber': TRCK, 'album_artist': TPE2, 'composer': TCOM, 'original_year': TORY, 'encoded_by': TENC, 'copyright': TCOP, 'isrc': TSRC, 'initial_key': TKEY, 'bpm': TBPM, } # Update the tags for tag, value in metadata.items(): tag_class = tag_map.get(tag.lower()) if tag_class: audio.tags.add(tag_class(encoding=3, text=str(value))) else: # Add as a custom tag audio.tags.add(TXXX(encoding=3, desc=tag, text=str(value))) # Save the changes audio.save() return get_mp3_metadata(mp3_path) except Exception as e: raise MediaMetadataError(f"Error updating MP3 metadata: {e}") # Generic File Metadata Functions def get_media_metadata(file_path: Union[str, Path]) -> Dict[str, Any]: """ Get metadata from a media file (supports images and MP3s). Args: file_path: Path to the media file Returns: Dictionary containing the file metadata Raises: MediaMetadataError: If the file type is not supported """ file_path = Path(file_path) ext = file_path.suffix.lower() if ext in ('.jpg', '.jpeg', '.tiff', '.png', '.webp'): return get_image_metadata(file_path) elif ext == '.mp3': return get_mp3_metadata(file_path) else: raise MediaMetadataError(f"Unsupported file type: {ext}") def update_media_metadata( file_path: Union[str, Path], metadata: Dict[str, Any], save_copy: bool = False, output_path: Optional[Union[str, Path]] = None ) -> Dict[str, Any]: """ Update metadata for a media file (supports images and MP3s). Args: file_path: Path to the media file metadata: Dictionary of metadata to update save_copy: If True, save to a new file instead of modifying the original output_path: Path to save the modified file (required if save_copy is True) Returns: Dictionary containing the updated metadata Raises: MediaMetadataError: If the file type is not supported or there's an error """ file_path = Path(file_path) ext = file_path.suffix.lower() if ext in ('.jpg', '.jpeg', '.tiff', '.png', '.webp'): return update_image_metadata(file_path, metadata, save_copy, output_path) elif ext == '.mp3': return update_mp3_metadata(file_path, metadata, save_copy, output_path) else: raise MediaMetadataError(f"Unsupported file type: {ext}")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sandraschi/windows-operations-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server