# Media Management Architecture This document describes the consumer-agnostic media architecture used in the platform. ## Overview ``` ┌─────────────────────────────────────────────────────────────────────┐ │ CONSUMER MODULES │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Catalog │ │ Art Gallery │ │ Future │ │ │ │ │ │ (future) │ │ Module │ │ │ │ ProductMedia│ │ GalleryMedia│ │ XxxMedia │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ └──────────────────┼──────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ CMS Module │ │ │ │ │ │ │ │ MediaFile (generic, consumer-agnostic storage) │ │ │ │ MediaService (upload, download, metadata management) │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` ## Design Principles ### 1. Consumer-Agnostic Storage The CMS module provides **generic media storage** without knowing what entities will use the media files. This means: - `MediaFile` stores file metadata (path, size, type, dimensions) - `MediaService` handles upload, download, and file operations - CMS has **no knowledge** of products, galleries, or any specific consumers ### 2. Consumer-Owned Associations Each module that needs media defines its **own association table**: - **Catalog**: `ProductMedia` links products to media files - **Future Art Gallery**: Would define `GalleryMedia` - **Future Blog**: Would define `PostMedia` This follows the principle: **The consumer knows what it needs, the provider doesn't need to know who uses it.** ### 3. Correct Dependency Direction ``` WRONG (Hidden Dependency): CMS → Catalog (CMS knows about Product) CORRECT: Catalog → CMS (Catalog uses MediaFile) ``` Optional modules (catalog) depend on core modules (cms), never the reverse. ## Key Components ### MediaFile Model (CMS) ```python # app/modules/cms/models/media.py class MediaFile(Base, TimestampMixin): """Generic store media file - consumer-agnostic.""" __tablename__ = "media_files" id = Column(Integer, primary_key=True) store_id = Column(Integer, ForeignKey("stores.id"), nullable=False) # File identification filename = Column(String(255), nullable=False) # UUID-based original_filename = Column(String(255)) file_path = Column(String(500), nullable=False) # File properties media_type = Column(String(20), nullable=False) # image, video, document mime_type = Column(String(100)) file_size = Column(Integer) # Image/video dimensions width = Column(Integer) height = Column(Integer) # Metadata alt_text = Column(String(500)) description = Column(Text) folder = Column(String(100), default="general") # Usage tracking (updated by consumers) usage_count = Column(Integer, default=0) # Note: Consumer-specific associations (ProductMedia, etc.) are defined # in their respective modules. CMS doesn't know about specific consumers. ``` ### MediaService (CMS) The `MediaService` provides generic operations: ```python # app/modules/cms/services/media_service.py class MediaService: """Generic media operations - consumer-agnostic.""" async def upload_file(self, db, store_id, file_content, filename, folder="general"): """Upload a file to store's media library.""" ... def get_media(self, db, store_id, media_id): """Get a media file by ID.""" ... def get_media_library(self, db, store_id, skip=0, limit=100, **filters): """List store's media files with filtering.""" ... def update_media_metadata(self, db, store_id, media_id, **metadata): """Update file metadata (alt_text, description, etc.).""" ... def delete_media(self, db, store_id, media_id): """Delete a media file.""" ... ``` ### ProductMedia Model (Catalog) ```python # app/modules/catalog/models/product_media.py class ProductMedia(Base, TimestampMixin): """Product-specific media association - owned by catalog.""" __tablename__ = "product_media" id = Column(Integer, primary_key=True) product_id = Column(Integer, ForeignKey("products.id", ondelete="CASCADE")) media_id = Column(Integer, ForeignKey("media_files.id", ondelete="CASCADE")) # Usage type: main_image, gallery, variant, thumbnail, swatch usage_type = Column(String(50), nullable=False, default="gallery") display_order = Column(Integer, default=0) variant_id = Column(Integer) # For variant-specific images # Relationships product = relationship("Product", back_populates="media_associations") media = relationship("MediaFile", lazy="joined") ``` ### ProductMediaService (Catalog) ```python # app/modules/catalog/services/product_media_service.py class ProductMediaService: """Product-media association operations - catalog-specific.""" def attach_media_to_product(self, db, store_id, product_id, media_id, usage_type="gallery", display_order=0): """Attach a media file to a product.""" # Verify ownership, create ProductMedia association ... def detach_media_from_product(self, db, store_id, product_id, media_id, usage_type=None): """Detach media from a product.""" ... def get_product_media(self, db, product_id, usage_type=None): """Get media associations for a product.""" ... def set_main_image(self, db, store_id, product_id, media_id): """Set the main image for a product.""" ... ``` ## Adding Media Support to a New Module When creating a new module that needs media (e.g., an art gallery module): ### Step 1: Create Your Association Model ```python # app/modules/art_gallery/models/gallery_media.py from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship from app.core.database import Base class GalleryMedia(Base): """Gallery-specific media association.""" __tablename__ = "gallery_media" id = Column(Integer, primary_key=True) artwork_id = Column(Integer, ForeignKey("artworks.id", ondelete="CASCADE")) media_id = Column(Integer, ForeignKey("media_files.id", ondelete="CASCADE")) # Gallery-specific fields is_primary = Column(Boolean, default=False) caption = Column(String(500)) # Relationships artwork = relationship("Artwork", back_populates="media_associations") media = relationship("MediaFile", lazy="joined") ``` ### Step 2: Create Your Service ```python # app/modules/art_gallery/services/gallery_media_service.py from app.modules.cms.models import MediaFile from app.modules.art_gallery.models import GalleryMedia, Artwork class GalleryMediaService: def attach_media_to_artwork(self, db, artist_id, artwork_id, media_id, **kwargs): # Verify artwork belongs to artist # Verify media belongs to artist (store_id) # Create GalleryMedia association ... ``` ### Step 3: Use CMS MediaService for File Operations ```python from app.modules.cms.services.media_service import media_service # Upload a new file media_file = await media_service.upload_file( db=db, store_id=artist_id, file_content=file_bytes, filename="artwork.jpg", folder="artworks", ) # Then attach it to your entity gallery_media_service.attach_media_to_artwork( db=db, artist_id=artist_id, artwork_id=artwork.id, media_id=media_file.id, is_primary=True, ) ``` ## Benefits of This Architecture 1. **Module Independence**: Catalog can be disabled without affecting CMS 2. **Extensibility**: New modules easily add media support 3. **No Hidden Dependencies**: Dependencies flow in one direction 4. **Clean Separation**: CMS handles storage, consumers handle associations 5. **Testability**: Can test CMS without any consumer modules 6. **Single Responsibility**: Each module owns its domain ## Anti-Patterns to Avoid ### Don't: Add Consumer References to MediaFile ```python # BAD - Creates hidden dependency class MediaFile(Base): # CMS now depends on catalog! product_associations = relationship("ProductMedia", ...) ``` ### Don't: Put Consumer Logic in MediaService ```python # BAD - CMS shouldn't know about products class MediaService: def attach_to_product(self, db, product_id, media_id): from app.modules.catalog.models import ProductMedia # Wrong! ``` ### Don't: Query Consumer Models from CMS ```python # BAD - Hidden dependency def get_media_usage(self, db, media_id): from app.modules.catalog.models import ProductMedia # Wrong! return db.query(ProductMedia).filter(...).all() ``` ## Related Documentation - [Module System Architecture](module-system.md) - Module structure and dependencies - [Cross-Module Import Rules](cross-module-import-rules.md) - Import restrictions - [Audit Provider Pattern](audit-provider-pattern.md) - Similar provider pattern for audit logging