iOS App (complete rebuild): - Audited all fitness API endpoints against live responses - Models match exact API field names (snapshot_ prefixes, UUID strings) - FoodEntry uses computed properties (foodName, calories, etc.) wrapping snapshot fields - Flexible Int/Double decoding for all numeric fields - AI assistant with raw JSON state management (JSONSerialization, not Codable) - Home dashboard with custom background, frosted glass calorie widget - Fitness: Today/Templates/Goals/Foods tabs - Food search with recent + all sections - Meal sections with colored accent bars, swipe to delete - 120fps ProMotion, iOS 17+ @Observable Podcast/Media Service: - FastAPI backend for podcast RSS + local audiobook folders - Shows, episodes, playback progress, queue management - RSS feed fetching with feedparser + ETag support - Local folder scanning with mutagen for audio metadata - HTTP Range streaming for local audio files - Playback events logging (play/pause/seek/complete) - Reuses brain's PostgreSQL + Redis - media_ prefixed tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
4.0 KiB
Python
110 lines
4.0 KiB
Python
"""SQLAlchemy models for the media service."""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
Column, String, Text, Integer, BigInteger, Boolean, Float,
|
|
DateTime, ForeignKey, Index, UniqueConstraint,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class Show(Base):
|
|
__tablename__ = "media_shows"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
user_id = Column(String(64), nullable=False, index=True)
|
|
title = Column(String(500), nullable=False)
|
|
author = Column(String(500))
|
|
description = Column(Text)
|
|
artwork_url = Column(Text)
|
|
feed_url = Column(Text)
|
|
local_path = Column(Text)
|
|
show_type = Column(String(20), nullable=False, default="podcast")
|
|
etag = Column(String(255))
|
|
last_modified = Column(String(255))
|
|
last_fetched_at = Column(DateTime)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
episodes = relationship("Episode", back_populates="show", cascade="all, delete-orphan")
|
|
|
|
|
|
class Episode(Base):
|
|
__tablename__ = "media_episodes"
|
|
__table_args__ = (
|
|
UniqueConstraint("show_id", "guid", name="uq_media_episodes_show_guid"),
|
|
Index("idx_media_episodes_show", "show_id"),
|
|
Index("idx_media_episodes_published", "published_at"),
|
|
)
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
show_id = Column(UUID(as_uuid=True), ForeignKey("media_shows.id", ondelete="CASCADE"))
|
|
user_id = Column(String(64), nullable=False)
|
|
title = Column(String(1000))
|
|
description = Column(Text)
|
|
audio_url = Column(Text)
|
|
duration_seconds = Column(Integer)
|
|
file_size_bytes = Column(BigInteger)
|
|
published_at = Column(DateTime)
|
|
guid = Column(String(500))
|
|
artwork_url = Column(Text)
|
|
is_downloaded = Column(Boolean, default=False)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
show = relationship("Show", back_populates="episodes")
|
|
progress = relationship("Progress", back_populates="episode", uselist=False, cascade="all, delete-orphan")
|
|
|
|
|
|
class Progress(Base):
|
|
__tablename__ = "media_progress"
|
|
__table_args__ = (
|
|
UniqueConstraint("user_id", "episode_id", name="uq_media_progress_user_episode"),
|
|
Index("idx_media_progress_user", "user_id"),
|
|
)
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
user_id = Column(String(64), nullable=False)
|
|
episode_id = Column(UUID(as_uuid=True), ForeignKey("media_episodes.id", ondelete="CASCADE"))
|
|
position_seconds = Column(Float, default=0)
|
|
duration_seconds = Column(Integer)
|
|
is_completed = Column(Boolean, default=False)
|
|
playback_speed = Column(Float, default=1.0)
|
|
last_played_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
episode = relationship("Episode", back_populates="progress")
|
|
|
|
|
|
class QueueItem(Base):
|
|
__tablename__ = "media_queue"
|
|
__table_args__ = (
|
|
UniqueConstraint("user_id", "episode_id", name="uq_media_queue_user_episode"),
|
|
Index("idx_media_queue_user_order", "user_id", "sort_order"),
|
|
)
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
user_id = Column(String(64), nullable=False)
|
|
episode_id = Column(UUID(as_uuid=True), ForeignKey("media_episodes.id", ondelete="CASCADE"))
|
|
sort_order = Column(Integer, nullable=False, default=0)
|
|
added_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
episode = relationship("Episode")
|
|
|
|
|
|
class PlaybackEvent(Base):
|
|
__tablename__ = "media_playback_events"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
user_id = Column(String(64), nullable=False)
|
|
episode_id = Column(UUID(as_uuid=True), ForeignKey("media_episodes.id", ondelete="CASCADE"))
|
|
event_type = Column(String(20), nullable=False)
|
|
position_seconds = Column(Float)
|
|
playback_speed = Column(Float, default=1.0)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
episode = relationship("Episode")
|